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
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -169,6 +169,7 @@ module Samagotchi
|
|
|
169
169
|
@chat_backend = nil
|
|
170
170
|
@chat_backend_mutex = Mutex.new
|
|
171
171
|
@side_judgments = SideJudgments.new
|
|
172
|
+
@judgments = build_judgments
|
|
172
173
|
@context_judge_after_turn = nil
|
|
173
174
|
# On mode's cards an answer brought while a turn ran: shown once it
|
|
174
175
|
# has ended (#show_context_cards_after_turn).
|
|
@@ -431,7 +432,11 @@ module Samagotchi
|
|
|
431
432
|
# System 1 (experimental, audit phase): the judgment client is read
|
|
432
433
|
# from config on every ask, and the running turn's cancel controller
|
|
433
434
|
# closes a judgment that is no longer wanted.
|
|
434
|
-
system_one:
|
|
435
|
+
system_one: lambda {
|
|
436
|
+
system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: Config.get("guardrails.system_one.mode")),
|
|
437
|
+
SystemOne::ApprovalQuestion::JUDGMENT, @session)
|
|
438
|
+
},
|
|
439
|
+
system_one_runs: -> { system_one_runs },
|
|
435
440
|
cancel_controller: -> { active_cancel_controller },
|
|
436
441
|
# A participant's turn: their name on the owner's card, their
|
|
437
442
|
# grant's mode for the turn's (GuardrailWiring#turn_mode).
|
|
@@ -2924,13 +2929,24 @@ module Samagotchi
|
|
|
2924
2929
|
tool_index: @tool_index,
|
|
2925
2930
|
peer_sources: @peer_sources,
|
|
2926
2931
|
prompt_sections: @prompt_sections,
|
|
2927
|
-
prompt_changed: -> { prompt_changed! }
|
|
2932
|
+
prompt_changed: -> { prompt_changed! },
|
|
2933
|
+
judgments: @judgments
|
|
2928
2934
|
)
|
|
2929
2935
|
# What plugins show while they load (an MCP server that didn't
|
|
2930
2936
|
# start) waits for the first turn, beside the load warnings: no UI
|
|
2931
2937
|
# is there yet, and the Engine isn't built.
|
|
2932
|
-
@extension_load.load_plugins(registries, failures: @plugin_failures)
|
|
2938
|
+
@extension_load.load_plugins(registries, failures: @plugin_failures) { warn_undeclared_judgments }
|
|
2939
|
+
end
|
|
2940
|
+
|
|
2941
|
+
# Held with the plugins' own notices, so every UI gets it: a worker's
|
|
2942
|
+
# stderr goes nowhere.
|
|
2943
|
+
def warn_undeclared_judgments
|
|
2944
|
+
@judgments.undeclared.each do |name|
|
|
2945
|
+
Log.warn(:config, "judgment_undeclared", judgment: name)
|
|
2946
|
+
hook_notify("system_one.judgments.#{name}: no loaded bundle declares it, so it does nothing", :warn, "config")
|
|
2947
|
+
end
|
|
2933
2948
|
end
|
|
2949
|
+
private :warn_undeclared_judgments
|
|
2934
2950
|
|
|
2935
2951
|
# Keep a plugin's new tool set (chi.replace_tools, from any thread)
|
|
2936
2952
|
# for the turn thread (PluginTasks#stage_tools).
|
|
@@ -3055,7 +3071,8 @@ module Samagotchi
|
|
|
3055
3071
|
return
|
|
3056
3072
|
end
|
|
3057
3073
|
|
|
3058
|
-
client = SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :toolkit)
|
|
3074
|
+
client = system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :toolkit),
|
|
3075
|
+
ToolkitSuggest::JUDGMENT, session, mode: Config.get("toolkit.suggest.mode").to_s)
|
|
3059
3076
|
return unless client
|
|
3060
3077
|
|
|
3061
3078
|
# D7: on a shared single-slot server audit's free ask would compete
|
|
@@ -3091,7 +3108,9 @@ module Samagotchi
|
|
|
3091
3108
|
state: { project: state[:project].dup.freeze, prompt: state[:prompt].dup.freeze }.freeze
|
|
3092
3109
|
)
|
|
3093
3110
|
threshold = Config.get("toolkit.suggest.threshold")
|
|
3111
|
+
runs = session.scratch ? nil : system_one_runs
|
|
3094
3112
|
pending = ToolkitSuggest::Pending.new(log: log, joined: joined,
|
|
3113
|
+
acted: runs && ->(judged, did) { runs.acted(judged.fields[:run_id], did) },
|
|
3095
3114
|
timeout_seconds: SystemOne::Client.configured_timeout_ms / 1000.0)
|
|
3096
3115
|
# On mode: cut by a Stop while the turn waits (Pending#wait), not
|
|
3097
3116
|
# by one after it went on; audit's ask is never cut.
|
|
@@ -3124,7 +3143,7 @@ module Samagotchi
|
|
|
3124
3143
|
# slow System 1 host (a local one) is the usual cause.
|
|
3125
3144
|
timeout_ms = SystemOne::Client.configured_timeout_ms
|
|
3126
3145
|
Log.warn(:toolkit, "toolkit_suggest_timeout", session_id: turn.session.id, timeout_ms: timeout_ms,
|
|
3127
|
-
reason: "System 1 gave no answer within
|
|
3146
|
+
reason: "System 1 gave no answer within system_one.timeout_ms")
|
|
3128
3147
|
@toolkit_binding.fell_back(turn.session, "System 1 took over #{timeout_ms} ms")
|
|
3129
3148
|
return
|
|
3130
3149
|
end
|
|
@@ -3210,6 +3229,46 @@ module Samagotchi
|
|
|
3210
3229
|
end
|
|
3211
3230
|
private :toolkit_suggestion_log
|
|
3212
3231
|
|
|
3232
|
+
# +client+ (nil for none) saving its asks as +judgment+'s runs. Anything
|
|
3233
|
+
# going wrong here leaves the client as it was: a run is never worth a
|
|
3234
|
+
# judgment.
|
|
3235
|
+
#
|
|
3236
|
+
# A scratch session's asks aren't saved; a participant's turn is never
|
|
3237
|
+
# judged, so it never comes here.
|
|
3238
|
+
# @param mode [String, nil] the use's mode, for its runs (Recording.wrap)
|
|
3239
|
+
def system_one_recording(client, judgment, session, mode: nil)
|
|
3240
|
+
return client if client.nil? || session&.scratch
|
|
3241
|
+
|
|
3242
|
+
context = SystemOne::Recording::Context.of(session, session_model: session && effective_model_name)
|
|
3243
|
+
SystemOne::Recording.wrap(client, judgment: judgment, runs: system_one_runs, context: context, recorded_mode: mode)
|
|
3244
|
+
rescue StandardError => e
|
|
3245
|
+
Log.warn(:system_one, "system_one_recording_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
3246
|
+
client
|
|
3247
|
+
end
|
|
3248
|
+
private :system_one_recording
|
|
3249
|
+
|
|
3250
|
+
# Bundles' judgments (chi.judgment), on this Engine's Runs and side asks.
|
|
3251
|
+
# A judgment's own host and model are for bundles only: core's clients
|
|
3252
|
+
# above never pass them (D2, JudgmentSettings).
|
|
3253
|
+
def build_judgments
|
|
3254
|
+
SystemOne::Judgments.new(
|
|
3255
|
+
client_for: lambda { |_name, settings|
|
|
3256
|
+
SystemOne::Client.from_config(host_registry: @host_registry, mode: settings.mode, host: settings.host,
|
|
3257
|
+
model: settings.client_model, tag: :plugins)
|
|
3258
|
+
},
|
|
3259
|
+
runs: -> { @session&.scratch ? SystemOne::Runs.new : system_one_runs },
|
|
3260
|
+
where: -> { @session && SystemOne::Recording::Context.of(@session, session_model: effective_model_name) },
|
|
3261
|
+
side: @side_judgments,
|
|
3262
|
+
shared_host_busy: ->(host) { turn_running? && system_one_shares_local_host?(host) },
|
|
3263
|
+
participant: -> { !turn_author.nil? }
|
|
3264
|
+
)
|
|
3265
|
+
end
|
|
3266
|
+
private :build_judgments
|
|
3267
|
+
|
|
3268
|
+
# Where this Engine's runs are saved; read each time, as the settings are.
|
|
3269
|
+
def system_one_runs = SystemOne::Runs.for_sessions_dir(session_state_dir)
|
|
3270
|
+
private :system_one_runs
|
|
3271
|
+
|
|
3213
3272
|
# D7: System 1's host is the session's model server, and a local one
|
|
3214
3273
|
# (a single-slot Splash or llama.cpp): the same name, or two hosts:
|
|
3215
3274
|
# entries for one server root (scheme, host, port).
|
|
@@ -3270,7 +3329,8 @@ module Samagotchi
|
|
|
3270
3329
|
parent_attached: context_parent_attached(session))
|
|
3271
3330
|
return if jobs.empty?
|
|
3272
3331
|
|
|
3273
|
-
client = SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :context)
|
|
3332
|
+
client = system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :context),
|
|
3333
|
+
ContextJudge::JUDGMENT, session, mode: settings.mode)
|
|
3274
3334
|
return unless client
|
|
3275
3335
|
|
|
3276
3336
|
choice = context_choice(session, log, settings)
|
|
@@ -3451,7 +3511,8 @@ module Samagotchi
|
|
|
3451
3511
|
turn_author: -> { turn_author },
|
|
3452
3512
|
# A plugin's prompt section is in the stable part: a full re-read.
|
|
3453
3513
|
cache_confirm: ->(line, changed) { CacheConfirm.ask(line: line, cost: :full, changed: changed, engine: self) },
|
|
3454
|
-
candidate_proposed: ->(_candidate) { judge_late_candidates }
|
|
3514
|
+
candidate_proposed: ->(_candidate) { judge_late_candidates },
|
|
3515
|
+
judgments: -> { @judgments }
|
|
3455
3516
|
)
|
|
3456
3517
|
end
|
|
3457
3518
|
|
|
@@ -43,11 +43,12 @@ module Samagotchi
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# Load the installed bundles' plugins into +registries+; one that fails
|
|
46
|
-
# goes to +failures+, and the rest still load.
|
|
46
|
+
# goes to +failures+, and the rest still load. The block runs after them,
|
|
47
|
+
# still loading: what it shows is held with theirs.
|
|
47
48
|
def load_plugins(registries, failures:)
|
|
48
49
|
@held_events = []
|
|
49
50
|
@loading = true
|
|
50
|
-
Plugin::Loader.load_installed(registries, failures: failures, settings: bundle_settings)
|
|
51
|
+
Plugin::Loader.load_installed(registries, failures: failures, settings: bundle_settings).tap { yield if block_given? }
|
|
51
52
|
ensure
|
|
52
53
|
@loading = false
|
|
53
54
|
end
|
|
@@ -44,12 +44,15 @@ module Samagotchi
|
|
|
44
44
|
# (a bundle's rules kept because it needs a newer chi)
|
|
45
45
|
# @param system_one [#call] → SystemOne::Client or nil: the judgment client
|
|
46
46
|
# (nil while `guardrails.system_one.mode` is off or no host is set)
|
|
47
|
+
# @param system_one_runs [#call] → SystemOne::Runs or nil: where labels
|
|
48
|
+
# and what an ask led to are saved
|
|
47
49
|
# @param cancel_controller [#call] → the running turn's CancellationController, nil
|
|
48
50
|
# @param participant [#call] (author) → the participant's
|
|
49
51
|
# Participants::Entry (participants.json: their name, their grant's
|
|
50
52
|
# mode), nil for none
|
|
51
53
|
def initialize(scratch:, hooks:, tools:, session:, model_key:, cancelled:, ask:, model_name: -> {},
|
|
52
|
-
notify: ->(_message) {}, system_one: -> {},
|
|
54
|
+
notify: ->(_message) {}, system_one: -> {}, system_one_runs: -> {}, cancel_controller: -> {},
|
|
55
|
+
participant: ->(_author) {})
|
|
53
56
|
@scratch = scratch
|
|
54
57
|
@hooks_lookup = hooks
|
|
55
58
|
@tools_lookup = tools
|
|
@@ -60,6 +63,7 @@ module Samagotchi
|
|
|
60
63
|
@ask = ask
|
|
61
64
|
@notify = notify
|
|
62
65
|
@system_one = system_one
|
|
66
|
+
@system_one_runs = system_one_runs
|
|
63
67
|
@cancel_controller = cancel_controller
|
|
64
68
|
@participant = participant
|
|
65
69
|
@turn_mode = nil
|
|
@@ -273,6 +277,7 @@ module Samagotchi
|
|
|
273
277
|
config_path: Samagotchi::ConfigFile.global_path,
|
|
274
278
|
hooks_dir: Hooks::Loader.hooks_dir,
|
|
275
279
|
context_dir: ContextSources.root,
|
|
280
|
+
runs_dir: File.join(File.dirname(@approvals.path, 2), "system_one"),
|
|
276
281
|
home_path: ChiHome.path,
|
|
277
282
|
shim_path: ChiShim.path
|
|
278
283
|
)
|
|
@@ -375,11 +380,13 @@ module Samagotchi
|
|
|
375
380
|
# judged is written after it, so the examples are always earlier asks.
|
|
376
381
|
entries = log.entries
|
|
377
382
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
383
|
+
# A scratch session saves no run, so nothing hangs a later line on one.
|
|
384
|
+
run_id = SystemOne::Runs.new_id(SystemOne::ApprovalQuestion::JUDGMENT) unless @session_lookup.call&.scratch
|
|
378
385
|
answers = begin
|
|
379
386
|
client.ask(state: SystemOne::ApprovalQuestion.state(approval, session_title: @session_lookup.call&.title,
|
|
380
387
|
entries: entries),
|
|
381
388
|
questions: { SystemOne::ApprovalQuestion::ID => SystemOne::ApprovalQuestion.question },
|
|
382
|
-
cancel_controller: @cancel_controller.call)
|
|
389
|
+
cancel_controller: @cancel_controller.call, run_id: run_id)
|
|
383
390
|
rescue LLM::RequestCancelled
|
|
384
391
|
raise
|
|
385
392
|
rescue StandardError => e
|
|
@@ -393,13 +400,14 @@ module Samagotchi
|
|
|
393
400
|
auto = system_one_auto_allow?(client, approval, p, entries, targets: targets)
|
|
394
401
|
decision = system_one_decision(client, auto)
|
|
395
402
|
|
|
396
|
-
id = log.judgment(session_id: @session_lookup.call&.id,
|
|
403
|
+
id = log.judgment(id: run_id, session_id: @session_lookup.call&.id,
|
|
397
404
|
rule: approval.rule, source: approval.source, tool: approval.tool,
|
|
398
405
|
**SystemOne::ApprovalQuestion.call_fields(approval),
|
|
399
406
|
repo_name: approval.repo_name,
|
|
400
407
|
p: p, threshold: Config.get("guardrails.system_one.threshold"), model: client.model,
|
|
401
408
|
latency_ms: latency_ms, decision: decision, origin: "live")
|
|
402
409
|
# A judgment the log cannot hold is no judgment: the user is asked.
|
|
410
|
+
system_one_run_facts(run_id, approval, targets, id.nil? && auto ? "asked" : decision)
|
|
403
411
|
{ id: id, p: p, auto: auto && !id.nil? }
|
|
404
412
|
rescue LLM::RequestCancelled
|
|
405
413
|
raise
|
|
@@ -409,6 +417,22 @@ module Samagotchi
|
|
|
409
417
|
nil
|
|
410
418
|
end
|
|
411
419
|
|
|
420
|
+
# The saved run's later lines: what the ask led to, and what the gates
|
|
421
|
+
# say of it now, which can't be recomputed later (a rule list changes):
|
|
422
|
+
# a later comparison must tell a sweep of gated asks from ungated ones.
|
|
423
|
+
def system_one_run_facts(run_id, approval, targets, what)
|
|
424
|
+
runs = @system_one_runs.call
|
|
425
|
+
return unless run_id && runs&.on?
|
|
426
|
+
|
|
427
|
+
rule = approval.rule.to_s
|
|
428
|
+
runs.gates(run_id, rule: rule, rule_listed: system_one_rules.include?(rule),
|
|
429
|
+
self_protect_refused: SystemOne::SelfProtect.refuse?(approval, targets: targets),
|
|
430
|
+
threshold: Config.get("guardrails.system_one.threshold"))
|
|
431
|
+
runs.acted(run_id, what)
|
|
432
|
+
rescue StandardError => e
|
|
433
|
+
Samagotchi::Log.warn(:guardrails, "system_one_run_facts_failed", error: "#{e.class}: #{e.message}")
|
|
434
|
+
end
|
|
435
|
+
|
|
412
436
|
# What the judgment line says it did: "auto" (System 1 answered for the
|
|
413
437
|
# user), "asked" (on mode, the user decides), "audit" (nothing decided).
|
|
414
438
|
def system_one_decision(client, auto)
|
|
@@ -450,43 +474,24 @@ module Samagotchi
|
|
|
450
474
|
[]
|
|
451
475
|
end
|
|
452
476
|
|
|
453
|
-
# The
|
|
454
|
-
#
|
|
455
|
-
# agent's answer is not the user's: its allow labels nothing, and its
|
|
456
|
-
# deny is a label only where the parent could have allowed — a parent
|
|
457
|
-
# that was not allowed to permit says nothing about what the user would
|
|
458
|
-
# have done (docs/guardrails.md, "System 1").
|
|
477
|
+
# The answer to that ask, as a label on the judgment and on its saved run
|
|
478
|
+
# (SystemOne::ApprovalLabel: which answers are labels, how strong).
|
|
459
479
|
def system_one_label(judgment, answer, payload)
|
|
460
480
|
id = judgment && judgment[:id]
|
|
461
481
|
return nil if id.nil?
|
|
462
482
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
else
|
|
471
|
-
index = answer.selected_indices.first
|
|
472
|
-
label = if index && index < scopes.size
|
|
473
|
-
scopes[index] == "once" ? "allow_once" : "allow_wide"
|
|
474
|
-
else
|
|
475
|
-
"deny"
|
|
476
|
-
end
|
|
477
|
-
end
|
|
478
|
-
system_one_log.label(id, label, question_id: (answer.id if answer.is_a?(Samagotchi::QuestionAnswer)))
|
|
483
|
+
label = SystemOne::ApprovalLabel.of(answer, scopes: Array(payload[:approval].scopes),
|
|
484
|
+
parent_may_permit: -> { parent_may_permit?(payload) })
|
|
485
|
+
return nil if label.nil?
|
|
486
|
+
|
|
487
|
+
written = system_one_log.label(id, label.value, question_id: (answer.id if answer.is_a?(Samagotchi::QuestionAnswer)))
|
|
488
|
+
SystemOne::ApprovalLabel.save(@system_one_runs.call, id, label)
|
|
489
|
+
written
|
|
479
490
|
rescue StandardError => e
|
|
480
491
|
Samagotchi::Log.warn(:guardrails, "system_one_label_failed", error: "#{e.class}: #{e.message}")
|
|
481
492
|
nil
|
|
482
493
|
end
|
|
483
494
|
|
|
484
|
-
# Whether a parent's answer picked one of the offered scopes.
|
|
485
|
-
def parent_allowed?(answer, scopes)
|
|
486
|
-
index = answer.selected_indices.first
|
|
487
|
-
!index.nil? && index < scopes.size
|
|
488
|
-
end
|
|
489
|
-
|
|
490
495
|
# Whether this session's parent level lets a parent allow a call at all,
|
|
491
496
|
# and the ask is not one only the user may answer.
|
|
492
497
|
def parent_may_permit?(payload)
|
|
@@ -12,8 +12,9 @@ module Samagotchi
|
|
|
12
12
|
# words against, as this process's environment and config.yml name them
|
|
13
13
|
# (read each call). Each surface keeps its set:
|
|
14
14
|
# .protected the config dir, the hooks dir, the approval store's
|
|
15
|
-
# dir, the installed bundles' dir, attached context
|
|
16
|
-
# ask only the user may answer
|
|
15
|
+
# dir, the installed bundles' dir, attached context,
|
|
16
|
+
# System 1's saved runs: an ask only the user may answer
|
|
17
|
+
# (ParentApprovals.chi_files?)
|
|
17
18
|
# .touches .protected and the session repo's git hooks dir: a
|
|
18
19
|
# rule's touches: chi_dirs (Targets#touches_chi?)
|
|
19
20
|
# .self_protect .protected and the whole state dir, resolved: what
|
|
@@ -27,11 +28,13 @@ module Samagotchi
|
|
|
27
28
|
module_function
|
|
28
29
|
|
|
29
30
|
# chi's config dir, the hooks dir, the approval store's dir, the
|
|
30
|
-
# installed bundles' dir and
|
|
31
|
+
# installed bundles' dir, attached context and System 1's saved runs
|
|
32
|
+
# (labels there may train a model: no agent writes them).
|
|
31
33
|
def protected
|
|
32
34
|
hooks = begin Hooks::Loader.hooks_dir rescue nil end
|
|
33
35
|
bundles = begin MemoryPaths.bundles_dir rescue nil end
|
|
34
|
-
[ConfigFile.config_dir, hooks, File.join(Paths.state_dir, "guardrails"), bundles, File.join(Paths.state_dir, "context")
|
|
36
|
+
[ConfigFile.config_dir, hooks, File.join(Paths.state_dir, "guardrails"), bundles, File.join(Paths.state_dir, "context"),
|
|
37
|
+
File.join(Paths.state_dir, "system_one")]
|
|
35
38
|
.compact.map { |dir| dir.to_s.chomp("/") }.reject(&:empty?).uniq
|
|
36
39
|
end
|
|
37
40
|
|
|
@@ -9,7 +9,7 @@ module Samagotchi
|
|
|
9
9
|
# Outside, ShellPaths, RmTargets, ReviewPaths, PairCommand, Targets,
|
|
10
10
|
# ParentApprovals, System 1's SelfProtect). Each surface keeps its own
|
|
11
11
|
# predicate; this only says where a path lands and whether it is in a
|
|
12
|
-
# dir. docs/internals/tool-
|
|
12
|
+
# dir. docs/internals/tool-output-limits.md maps which surface uses which.
|
|
13
13
|
module PathResolve
|
|
14
14
|
# How many symlinks .real follows on a path that doesn't exist
|
|
15
15
|
# (about the kernel's own ELOOP limit).
|
|
@@ -11,9 +11,10 @@ module Samagotchi
|
|
|
11
11
|
# lexical answer and where the write lands, a vote when either is
|
|
12
12
|
# protected).
|
|
13
13
|
# deny: the approval store's dir, installed bundles (.bundles/:
|
|
14
|
-
# hooks, rules, manifests)
|
|
14
|
+
# hooks, rules, manifests), attached context (a source's
|
|
15
15
|
# command runs later, outside the gate; chi context writes
|
|
16
|
-
# it)
|
|
16
|
+
# it) and System 1's saved runs (a label there may train a
|
|
17
|
+
# model); nothing legitimate writes there through the file tools.
|
|
17
18
|
# ask (once / session): config.yml, home.json (the chi home: the
|
|
18
19
|
# Ruby every chi launch boots), the chi shim (ChiShim: the
|
|
19
20
|
# `chi` every tool runs) and the plain hooks dir. The
|
|
@@ -31,13 +32,15 @@ module Samagotchi
|
|
|
31
32
|
# @param config_path [String] config.yml
|
|
32
33
|
# @param hooks_dir [String] the plain hooks dir
|
|
33
34
|
# @param context_dir [String, nil] <state dir>/context (ContextSources.root)
|
|
35
|
+
# @param runs_dir [String, nil] <state dir>/system_one (SystemOne::Runs)
|
|
34
36
|
# @param home_path [String, nil] home.json (ChiHome.path)
|
|
35
37
|
# @param shim_path [String, nil] the chi shim (ChiShim.path)
|
|
36
|
-
def initialize(store_dir:, bundles_dir:, config_path:, hooks_dir:, context_dir: nil, home_path: nil, shim_path: nil)
|
|
38
|
+
def initialize(store_dir:, bundles_dir:, config_path:, hooks_dir:, context_dir: nil, runs_dir: nil, home_path: nil, shim_path: nil)
|
|
37
39
|
@deny = [
|
|
38
40
|
["guardrail-store", store_dir, "the approval store belongs to the user"],
|
|
39
41
|
["installed-bundles", bundles_dir, "installed bundles (hooks, rules, manifests) are changed by chi bundle install"],
|
|
40
|
-
["chi-context", context_dir, "attached context sources run commands; change them with chi context"]
|
|
42
|
+
["chi-context", context_dir, "attached context sources run commands; change them with chi context"],
|
|
43
|
+
["system-one-runs", runs_dir, "System 1's saved runs are chi's own record"]
|
|
41
44
|
]
|
|
42
45
|
@ask = [
|
|
43
46
|
["chi-config", config_path, "changes chi's config.yml"],
|
|
@@ -39,7 +39,7 @@ module Samagotchi
|
|
|
39
39
|
user-only). --threshold overrides guardrails.system_one.threshold
|
|
40
40
|
for the table; --model overrides the configured model for this run.
|
|
41
41
|
--dry-run judges and prints the table without writing a line. Needs
|
|
42
|
-
guardrails.system_one.mode audit or on and a
|
|
42
|
+
guardrails.system_one.mode audit or on and a system_one.host
|
|
43
43
|
(exit 2 without one).
|
|
44
44
|
TEXT
|
|
45
45
|
|
|
@@ -146,10 +146,10 @@ module Samagotchi
|
|
|
146
146
|
threshold = threshold(options[:threshold])
|
|
147
147
|
return usage_error(threshold) if threshold.is_a?(String)
|
|
148
148
|
|
|
149
|
-
client = @client || SystemOne::Client.from_config(host_registry: HostRegistry.new, model: options[:model])
|
|
149
|
+
client = @client || SystemOne::Client.from_config(host_registry: HostRegistry.new, mode: Config.get("guardrails.system_one.mode"), model: options[:model])
|
|
150
150
|
if client.nil?
|
|
151
151
|
error_line("chi guardrails replay: no System 1 client — set guardrails.system_one.mode to audit or on " \
|
|
152
|
-
"and
|
|
152
|
+
"and system_one.host in config.yml")
|
|
153
153
|
return CLI::Exit::USAGE
|
|
154
154
|
end
|
|
155
155
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cli/command"
|
|
4
|
+
require_relative "cli/exit"
|
|
5
|
+
require_relative "cli/flags"
|
|
6
|
+
require_relative "session"
|
|
7
|
+
require_relative "system_one/runs"
|
|
8
|
+
|
|
9
|
+
module Samagotchi
|
|
10
|
+
# `chi judgments`: what System 1's saved runs hold (docs/system-one.md).
|
|
11
|
+
# Reads only: it never asks a model or changes a run.
|
|
12
|
+
class JudgmentsCommand
|
|
13
|
+
include CLI::Command
|
|
14
|
+
|
|
15
|
+
USAGE = <<~TEXT
|
|
16
|
+
Usage: chi judgments ls
|
|
17
|
+
chi judgments report NAME [--days N]
|
|
18
|
+
System 1's saved asks on this machine. ls: each judgment's digests,
|
|
19
|
+
runs, labels, failures, models and size. report: one judgment's counts,
|
|
20
|
+
p histogram and labels by value, over N days (default: all that's kept).
|
|
21
|
+
TEXT
|
|
22
|
+
|
|
23
|
+
SUBCOMMANDS = %w[ls report].freeze
|
|
24
|
+
FLAGS = { "ls" => CLI::Flags.new(args: false), "report" => CLI::Flags.new { |flags| flags.value "--days" } }.freeze
|
|
25
|
+
BINS = 10
|
|
26
|
+
|
|
27
|
+
# @param argv [Array<String>] the arguments after "judgments"
|
|
28
|
+
# @param sessions_dir [String, nil] the sessions folder (a spec seam)
|
|
29
|
+
# @param now [Time] the report's clock (a spec seam)
|
|
30
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr, sessions_dir: nil, now: Time.now)
|
|
31
|
+
@argv = argv.dup
|
|
32
|
+
@stdout = stdout
|
|
33
|
+
@stderr = stderr
|
|
34
|
+
@store = SystemOne::RunsStore.new(File.dirname(sessions_dir || Session.default_state_dir))
|
|
35
|
+
@now = now
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def run
|
|
39
|
+
@sub = @argv.first
|
|
40
|
+
if @sub.nil? || HELP_WORDS.include?(@sub)
|
|
41
|
+
@stdout.puts(USAGE)
|
|
42
|
+
return CLI::Exit::OK
|
|
43
|
+
end
|
|
44
|
+
return usage_error("unknown subcommand #{@sub}") unless SUBCOMMANDS.include?(@sub)
|
|
45
|
+
|
|
46
|
+
parsed = parse_flags(FLAGS.fetch(@sub), @argv[1..])
|
|
47
|
+
return parsed if parsed.is_a?(Integer)
|
|
48
|
+
|
|
49
|
+
@sub == "ls" ? ls : report(parsed)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def command_name = @sub ? "chi judgments #{@sub}" : "chi judgments"
|
|
55
|
+
|
|
56
|
+
def ls
|
|
57
|
+
names = @store.names
|
|
58
|
+
if names.empty?
|
|
59
|
+
@stdout.puts("No saved runs.")
|
|
60
|
+
return CLI::Exit::OK
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
names.each { |name| @stdout.puts(ls_line(name)) }
|
|
64
|
+
CLI::Exit::OK
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ls_line(name)
|
|
68
|
+
runs, later = @store.lines(name).partition { |line| line["kind"] == "run" }
|
|
69
|
+
models = runs.filter_map { |run| run["served_model"] || run["model"] }.uniq
|
|
70
|
+
"#{name} digests #{@store.versions(name).size} runs #{runs.size} " \
|
|
71
|
+
"labels #{last_labels(runs, later).size} failures #{runs.count { |run| run["failure"] }} " \
|
|
72
|
+
"models #{models.empty? ? "none" : models.join(", ")} #{size_text(@store.size(name))}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def size_text(bytes)
|
|
76
|
+
return "#{bytes} B" if bytes < 1024
|
|
77
|
+
return format("%.1f KB", bytes / 1024.0) if bytes < 1024 * 1024
|
|
78
|
+
|
|
79
|
+
format("%.1f MB", bytes / (1024.0 * 1024))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def report(parsed)
|
|
83
|
+
name = parsed.args.first
|
|
84
|
+
return usage_error("report takes one judgment's name (see chi judgments ls)") if parsed.args.size != 1
|
|
85
|
+
return usage_error("not a judgment's name: #{name}") unless name.match?(SystemOne::RunsStore::NAME)
|
|
86
|
+
|
|
87
|
+
days = parsed.options[:days]
|
|
88
|
+
return usage_error("--days takes a positive whole number") if days && !days.to_s.match?(/\A[1-9]\d*\z/)
|
|
89
|
+
|
|
90
|
+
since = days && (@now.utc - (Integer(days, 10) * 86_400)).strftime("%F")
|
|
91
|
+
runs, later = @store.lines(name).partition { |line| line["kind"] == "run" }
|
|
92
|
+
runs = runs.select { |run| since.nil? || run["at"].to_s[0, 10] >= since }
|
|
93
|
+
if runs.empty?
|
|
94
|
+
@stdout.puts("No runs of #{name}#{" in the last #{days} days" if days}.")
|
|
95
|
+
return CLI::Exit::OK
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
print_report(name, days, runs, later)
|
|
99
|
+
CLI::Exit::OK
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def print_report(name, days, runs, later)
|
|
103
|
+
@stdout.puts("#{name} · #{days ? "last #{days} days" : "all kept"}")
|
|
104
|
+
asked = runs.count { |run| run["asked"] != false }
|
|
105
|
+
@stdout.puts("runs #{runs.size} asked #{asked} skipped #{runs.size - asked} failures #{runs.count { |run| run["failure"] }} " \
|
|
106
|
+
"truncated #{runs.count { |run| run["truncated"] }} test runs #{runs.count { |run| run["test_run"] }}")
|
|
107
|
+
failures = tally(runs.filter_map { |run| run["failure"] })
|
|
108
|
+
@stdout.puts("failures by class: #{failures}") unless failures.empty?
|
|
109
|
+
print_answers(runs)
|
|
110
|
+
last = last_labels(runs, later)
|
|
111
|
+
ids = runs.to_h { |run| [run["id"], true] }
|
|
112
|
+
@stdout.puts("labels: #{last.empty? ? "none" : tally(last.map { |line| "#{line["value"]} (#{line["by"]})" })}")
|
|
113
|
+
acted = later.select { |line| line["kind"] == "acted" && ids[line["run_id"]] }
|
|
114
|
+
@stdout.puts("acted: #{tally(acted.map { |line| line["what"] })}") unless acted.empty?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# One label per run and question: the last said. One whose run is gone
|
|
118
|
+
# or out of the window doesn't count.
|
|
119
|
+
def last_labels(runs, later)
|
|
120
|
+
ids = runs.to_h { |run| [run["id"], true] }
|
|
121
|
+
labels = later.select { |line| line["kind"] == "label" && ids[line["run_id"]] }
|
|
122
|
+
labels.to_h { |line| [[line["run_id"], line["question"]], line] }.values
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def print_answers(runs)
|
|
126
|
+
answers = runs.flat_map { |run| (run["answers"].is_a?(Hash) ? run["answers"] : {}).to_a }
|
|
127
|
+
answers.group_by(&:first).sort.each do |question, pairs|
|
|
128
|
+
values = pairs.map(&:last).grep(Hash)
|
|
129
|
+
probabilities = values.filter_map { |answer| answer["noul"] || answer["confidence"] }.grep(Numeric).select { |p| p.between?(0, 1) }
|
|
130
|
+
kind = values.first&.fetch("type", nil)
|
|
131
|
+
@stdout.puts("#{question} (#{kind}): #{probabilities.size} with a p#{", picks #{picks(values)}" if picks(values)}")
|
|
132
|
+
histogram(probabilities).each { |line| @stdout.puts(" #{line}") }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def picks(values)
|
|
137
|
+
chosen = values.filter_map { |answer| answer["choice"] }
|
|
138
|
+
chosen.empty? ? nil : tally(chosen)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def histogram(values)
|
|
142
|
+
return [] if values.empty?
|
|
143
|
+
|
|
144
|
+
counts = Array.new(BINS, 0)
|
|
145
|
+
values.each { |value| counts[[(value.to_f * BINS).floor, BINS - 1].min] += 1 }
|
|
146
|
+
counts.each_with_index.map do |count, bin|
|
|
147
|
+
format("%.1f–%.1f %5d", bin.to_f / BINS, (bin + 1).to_f / BINS, count)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def tally(values)
|
|
152
|
+
values.tally.sort_by { |value, count| [-count, value.to_s] }.map { |value, count| "#{value} #{count}" }.join(", ")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -193,6 +193,16 @@ module Samagotchi
|
|
|
193
193
|
attr_accessor :generation_guard
|
|
194
194
|
|
|
195
195
|
# The generation guard, if any; one that raises never breaks the turn.
|
|
196
|
+
# The model's context window as this loop knows it (ContextWindow.resolve
|
|
197
|
+
# on the client and the turn's setting): ToolRunner halves it for
|
|
198
|
+
# memory_read's overflow guard. nil when it can't be read.
|
|
199
|
+
# @return [ContextWindow::Resolved, nil]
|
|
200
|
+
def tool_output_window
|
|
201
|
+
ContextWindow.resolve(client: @client, model: @turn_settings&.model_name, setting: @turn_settings&.window_setting)
|
|
202
|
+
rescue StandardError
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
|
|
196
206
|
def guard_generation
|
|
197
207
|
@generation_guard&.call
|
|
198
208
|
rescue StandardError => e
|
|
@@ -306,7 +316,9 @@ module Samagotchi
|
|
|
306
316
|
# Precedence: an explicit override wins, then max_tool_output_chars
|
|
307
317
|
# (Config). A non-positive value falls back to
|
|
308
318
|
# DEFAULT_MAX_TOOL_OUTPUT_CHARS (there is intentionally no "unlimited" — live UIs get a
|
|
309
|
-
# bounded `output:` plus a truthful `output_truncated:` flag).
|
|
319
|
+
# bounded `output:` plus a truthful `output_truncated:` flag). The one
|
|
320
|
+
# exception is memory_read, which ToolRunner leaves out of this cap
|
|
321
|
+
# (memory.read_max_chars, 0: no fixed cap; a memory is read whole).
|
|
310
322
|
# Class-level so the chat loop resolves it the same way.
|
|
311
323
|
def self.resolve_output_char_cap(override)
|
|
312
324
|
parsed = (override || Samagotchi::Config.get("max_tool_output_chars")).to_i
|