samagotchi 0.85.0 → 0.86.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 +60 -1
- data/docs/architecture.md +34 -5
- data/docs/bundle-authoring.md +125 -0
- data/docs/cli.md +38 -3
- data/docs/configuration.md +6 -5
- data/docs/guardrails.md +63 -35
- data/docs/hooks.md +19 -3
- data/docs/internals/anthropic.md +2 -2
- data/docs/internals/bundles.md +11 -2
- data/docs/internals/context-telemetry.md +19 -1
- data/docs/internals/engine.md +1 -1
- data/docs/memory.md +1 -1
- data/docs/plugins.md +59 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +6 -6
- data/docs/sub-agent.md +35 -24
- data/docs/testing.md +7 -2
- data/lib/samagotchi/answer_command.rb +21 -25
- data/lib/samagotchi/bridge/client_event.rb +50 -0
- data/lib/samagotchi/bridge/sse_writer.rb +6 -2
- data/lib/samagotchi/bridge.rb +22 -11
- data/lib/samagotchi/bridge_client.rb +2 -2
- data/lib/samagotchi/bundle_command/check.rb +348 -0
- data/lib/samagotchi/bundle_command/scaffold.rb +135 -0
- data/lib/samagotchi/bundle_command/test_run.rb +89 -0
- data/lib/samagotchi/bundle_command.rb +125 -2
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +4 -4
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +4 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +4 -2
- data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
- data/lib/samagotchi/bundles/ruby/manifest.yml +11 -0
- data/lib/samagotchi/bundles/ruby/plugin.rb +74 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/client_id.rb +1 -1
- data/lib/samagotchi/config.rb +195 -115
- data/lib/samagotchi/context_status.rb +1 -1
- data/lib/samagotchi/continue_offer.rb +2 -1
- data/lib/samagotchi/engine.rb +4 -4
- data/lib/samagotchi/guardrail_wiring.rb +4 -0
- data/lib/samagotchi/guardrails/approval.rb +12 -2
- data/lib/samagotchi/guardrails/load_failures.rb +17 -3
- data/lib/samagotchi/guardrails/parent_approvals.rb +14 -70
- data/lib/samagotchi/guardrails/parent_policy.rb +258 -0
- data/lib/samagotchi/guardrails/verdict.rb +6 -3
- data/lib/samagotchi/hook_routes.rb +5 -3
- data/lib/samagotchi/hooks/bundle_loader.rb +3 -1
- data/lib/samagotchi/hooks/registry.rb +13 -6
- data/lib/samagotchi/hooks.rb +5 -0
- data/lib/samagotchi/kernel_loop.rb +5 -2
- data/lib/samagotchi/llm/anthropic_assembly.rb +1 -1
- data/lib/samagotchi/memory_bundle/shas.rb +127 -0
- data/lib/samagotchi/memory_bundle.rb +1 -0
- data/lib/samagotchi/parent_report.rb +87 -14
- data/lib/samagotchi/pending_question.rb +8 -4
- data/lib/samagotchi/plugin/api.rb +16 -0
- data/lib/samagotchi/plugin/context.rb +11 -5
- data/lib/samagotchi/plugin/loader.rb +5 -3
- data/lib/samagotchi/question_desk.rb +37 -24
- data/lib/samagotchi/queued_command.rb +5 -4
- data/lib/samagotchi/relay.rb +5 -1
- data/lib/samagotchi/relay_verifier.rb +4 -5
- data/lib/samagotchi/reply_wait.rb +14 -5
- data/lib/samagotchi/send_command.rb +34 -8
- data/lib/samagotchi/session_commands.rb +14 -3
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +1 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +13 -11
- data/lib/samagotchi/terminal_ui/thinking_line.rb +5 -9
- data/lib/samagotchi/terminal_ui.rb +12 -9
- data/lib/samagotchi/testing/boundary.rb +0 -3
- data/lib/samagotchi/testing/bundle.rb +7 -0
- data/lib/samagotchi/testing/host.rb +6 -4
- data/lib/samagotchi/testing/install.rb +119 -0
- data/lib/samagotchi/testing/recorder.rb +13 -4
- data/lib/samagotchi/testing.rb +1 -0
- data/lib/samagotchi/tools/builtins.rb +2 -1
- data/lib/samagotchi/tools/delegate_wait.rb +6 -1
- data/lib/samagotchi/tools/task_runtime.rb +5 -3
- data/lib/samagotchi/turn_checkpoint.rb +64 -2
- data/lib/samagotchi/turn_flow.rb +10 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +48 -10
- data/lib/samagotchi/web/public/chunk_router.js +8 -19
- data/lib/samagotchi/web/public/ctx.js +12 -26
- data/lib/samagotchi/web/public/stream_handlers.js +1 -1
- data/lib/samagotchi/web/server.rb +4 -0
- data/lib/samagotchi/worker.rb +19 -5
- data/lib/samagotchi/worker_inbound.rb +4 -4
- metadata +11 -2
- data/lib/samagotchi/guardrails/parent_continue.rb +0 -43
data/lib/samagotchi/config.rb
CHANGED
|
@@ -36,7 +36,10 @@ module Samagotchi
|
|
|
36
36
|
# handled by ConfigFile (same file, below), which reads the *same* parsed YAML
|
|
37
37
|
# through ConfigFile.read_yaml so the file is parsed once per change.
|
|
38
38
|
module Config
|
|
39
|
-
|
|
39
|
+
# invalid: what an enum's invalid value falls to, when not the default
|
|
40
|
+
# (guardrails.parent_answers: the strictest).
|
|
41
|
+
Entry = Struct.new(:key, :yaml_path, :type, :default, :expose, :enum_values, :yaml_aliases, :lifecycle, :invalid,
|
|
42
|
+
keyword_init: true) do
|
|
40
43
|
def env_key
|
|
41
44
|
"SAMAGOTCHI_" + yaml_path.map { |p| p.upcase }.join("_")
|
|
42
45
|
end
|
|
@@ -67,135 +70,143 @@ module Samagotchi
|
|
|
67
70
|
LEAF_RE = /\A[a-z0-9_]+\z/
|
|
68
71
|
# The agent CLIs the desktop helper lists in kitty and tmux by default.
|
|
69
72
|
DESKTOP_AGENTS = "claude|codex|gemini|aider|opencode|cursor-agent|amp|goose"
|
|
73
|
+
LIFECYCLES = %i[
|
|
74
|
+
new_session session_creation worker_start next_broadcast next_retention_sweep next_invocation next_lifecycle_decision
|
|
75
|
+
next_matching_use next_tool_or_approval process_start next_context_poll prompt_rebuild next_turn_start
|
|
76
|
+
next_request_or_turn_end next_update desktop_update web_start next_cli_run next_empty_answer_retry
|
|
77
|
+
next_history_access next_matching_turn_display_poll_write
|
|
78
|
+
].freeze
|
|
70
79
|
|
|
71
80
|
ENTRIES = [
|
|
72
81
|
# universal – env+config+cli
|
|
73
|
-
Entry.new(key: "default.model", yaml_path: %w[default model], type: :string, default: nil, expose: %i[env config cli]),
|
|
82
|
+
Entry.new(key: "default.model", yaml_path: %w[default model], type: :string, default: nil, expose: %i[env config cli], lifecycle: :new_session),
|
|
74
83
|
# The prompt profile for every model in this process (ModelProfile::NAMES).
|
|
75
84
|
# No config key: per-model and per-host profiles live in models: and hosts:.
|
|
76
85
|
Entry.new(key: "model.profile", yaml_path: %w[model profile], type: :enum, default: nil, expose: %i[env cli], enum_values: %w[qwen36 gemma4]),
|
|
77
|
-
Entry.new(key: "server.transport", yaml_path: %w[server transport], type: :enum, default: "llama_cpp", expose: %i[env config cli], enum_values: %w[llama_cpp mlx omlx]),
|
|
78
|
-
Entry.new(key: "server.host", yaml_path: %w[server host], type: :string, default: "localhost", expose: %i[env config cli]),
|
|
79
|
-
Entry.new(key: "server.port", yaml_path: %w[server port], type: :integer, default: 8080, expose: %i[env config cli]),
|
|
80
|
-
Entry.new(key: "server.open_timeout", yaml_path: %w[server open_timeout], type: :integer, default: 10, expose: %i[env config cli]),
|
|
81
|
-
Entry.new(key: "server.read_timeout", yaml_path: %w[server read_timeout], type: :integer, default: 600, expose: %i[env config cli]),
|
|
86
|
+
Entry.new(key: "server.transport", yaml_path: %w[server transport], type: :enum, default: "llama_cpp", expose: %i[env config cli], enum_values: %w[llama_cpp mlx omlx], lifecycle: :worker_start),
|
|
87
|
+
Entry.new(key: "server.host", yaml_path: %w[server host], type: :string, default: "localhost", expose: %i[env config cli], lifecycle: :worker_start),
|
|
88
|
+
Entry.new(key: "server.port", yaml_path: %w[server port], type: :integer, default: 8080, expose: %i[env config cli], lifecycle: :worker_start),
|
|
89
|
+
Entry.new(key: "server.open_timeout", yaml_path: %w[server open_timeout], type: :integer, default: 10, expose: %i[env config cli], lifecycle: :worker_start),
|
|
90
|
+
Entry.new(key: "server.read_timeout", yaml_path: %w[server read_timeout], type: :integer, default: 600, expose: %i[env config cli], lifecycle: :worker_start),
|
|
82
91
|
# Seconds a streamed answer may take to show its first text, reasoning
|
|
83
92
|
# or tool call, for every host; 0 = off. Unset: 120 on remote hosts,
|
|
84
93
|
# off on local ones. hosts.<name>.first_token_timeout wins.
|
|
85
|
-
Entry.new(key: "server.first_token_timeout", yaml_path: %w[server first_token_timeout], type: :integer, default: nil, expose: %i[env config]),
|
|
86
|
-
|
|
87
|
-
Entry.new(key: "recap.enabled", yaml_path: %w[recap enabled], type: :bool, default: nil, expose: %i[env config]),
|
|
88
|
-
Entry.new(key: "recap.model", yaml_path: %w[recap model], type: :string, default: nil, expose: %i[env config cli]),
|
|
89
|
-
Entry.new(key: "recap.base_url", yaml_path: %w[recap base_url], type: :string, default: nil, expose: %i[env config cli]),
|
|
90
|
-
Entry.new(key: "recap.host_ref", yaml_path: %w[recap host_ref], type: :string, default: nil, expose: %i[env config cli], yaml_aliases: %w[host]),
|
|
91
|
-
Entry.new(key: "recap.inactivity", yaml_path: %w[recap inactivity], type: :float, default: nil, expose: %i[env config cli]),
|
|
92
|
-
Entry.new(key: "recap.timeout", yaml_path: %w[recap timeout], type: :float, default: nil, expose: %i[env config cli]),
|
|
93
|
-
Entry.new(key: "recap.min_user_turns", yaml_path: %w[recap min_user_turns], type: :integer, default: nil, expose: %i[env config cli]),
|
|
94
|
+
Entry.new(key: "server.first_token_timeout", yaml_path: %w[server first_token_timeout], type: :integer, default: nil, expose: %i[env config], lifecycle: :worker_start),
|
|
95
|
+
|
|
96
|
+
Entry.new(key: "recap.enabled", yaml_path: %w[recap enabled], type: :bool, default: nil, expose: %i[env config], lifecycle: :worker_start),
|
|
97
|
+
Entry.new(key: "recap.model", yaml_path: %w[recap model], type: :string, default: nil, expose: %i[env config cli], lifecycle: :worker_start),
|
|
98
|
+
Entry.new(key: "recap.base_url", yaml_path: %w[recap base_url], type: :string, default: nil, expose: %i[env config cli], lifecycle: :worker_start),
|
|
99
|
+
Entry.new(key: "recap.host_ref", yaml_path: %w[recap host_ref], type: :string, default: nil, expose: %i[env config cli], yaml_aliases: %w[host], lifecycle: :worker_start),
|
|
100
|
+
Entry.new(key: "recap.inactivity", yaml_path: %w[recap inactivity], type: :float, default: nil, expose: %i[env config cli], lifecycle: :worker_start),
|
|
101
|
+
Entry.new(key: "recap.timeout", yaml_path: %w[recap timeout], type: :float, default: nil, expose: %i[env config cli], lifecycle: :worker_start),
|
|
102
|
+
Entry.new(key: "recap.min_user_turns", yaml_path: %w[recap min_user_turns], type: :integer, default: nil, expose: %i[env config cli], lifecycle: :worker_start),
|
|
94
103
|
# "N-M" or "N" sentences (1-10); unset = 2-4. Parsed by RecapPrompt.sentences_range.
|
|
95
|
-
Entry.new(key: "recap.sentences", yaml_path: %w[recap sentences], type: :string, default: nil, expose: %i[env config cli]),
|
|
104
|
+
Entry.new(key: "recap.sentences", yaml_path: %w[recap sentences], type: :string, default: nil, expose: %i[env config cli], lifecycle: :worker_start),
|
|
96
105
|
|
|
97
106
|
# chi broadcast (Broadcast::Recipients, Broadcast::Tags): a session that ended no turn in this many hours (and
|
|
98
107
|
# no worker or REPL runs) gets no broadcast; a ticket id in the note and in a session's branch or prompts
|
|
99
108
|
# delivers it there without triage.
|
|
100
|
-
Entry.new(key: "broadcast.active_hours", yaml_path: %w[broadcast active_hours], type: :float, default: 8.0, expose: %i[env config]),
|
|
101
|
-
Entry.new(key: "broadcast.ticket_pattern", yaml_path: %w[broadcast ticket_pattern], type: :string, default: '\b[A-Z][A-Z0-9]+-\d+\b', expose: %i[env config]),
|
|
109
|
+
Entry.new(key: "broadcast.active_hours", yaml_path: %w[broadcast active_hours], type: :float, default: 8.0, expose: %i[env config], lifecycle: :next_broadcast),
|
|
110
|
+
Entry.new(key: "broadcast.ticket_pattern", yaml_path: %w[broadcast ticket_pattern], type: :string, default: '\b[A-Z][A-Z0-9]+-\d+\b', expose: %i[env config], lifecycle: :next_broadcast),
|
|
102
111
|
# The triage model that judges a recipient no tag matched (Broadcast::TriageModel): unset, recap.*'s, then
|
|
103
112
|
# default.model. At most triage_parallel requests at a time, all within triage_deadline seconds (the rest are
|
|
104
113
|
# delivered unchecked); a logprob P(yes) under threshold skips the session.
|
|
105
|
-
Entry.new(key: "broadcast.triage_model", yaml_path: %w[broadcast triage_model], type: :string, default: nil, expose: %i[env config]),
|
|
106
|
-
Entry.new(key: "broadcast.triage_host_ref", yaml_path: %w[broadcast triage_host_ref], type: :string, default: nil, expose: %i[env config]),
|
|
107
|
-
Entry.new(key: "broadcast.triage_base_url", yaml_path: %w[broadcast triage_base_url], type: :string, default: nil, expose: %i[env config]),
|
|
108
|
-
Entry.new(key: "broadcast.triage_parallel", yaml_path: %w[broadcast triage_parallel], type: :integer, default: 4, expose: %i[env config]),
|
|
109
|
-
Entry.new(key: "broadcast.triage_deadline", yaml_path: %w[broadcast triage_deadline], type: :float, default: 20.0, expose: %i[env config]),
|
|
110
|
-
Entry.new(key: "broadcast.threshold", yaml_path: %w[broadcast threshold], type: :float, default: 0.5, expose: %i[env config]),
|
|
111
|
-
|
|
112
|
-
Entry.new(key: "session.retention_days", yaml_path: %w[session retention_days], type: :integer, default: 14, expose: %i[env config cli]),
|
|
113
|
-
Entry.new(key: "session.max_count", yaml_path: %w[session max_count], type: :integer, default: 500, expose: %i[env config cli]),
|
|
114
|
-
Entry.new(key: "session.keep_status", yaml_path: %w[session keep_status], type: :string, default: "", expose: %i[env config cli]),
|
|
115
|
-
Entry.new(key: "session.sweep_interval_hours", yaml_path: %w[session sweep_interval_hours], type: :integer, default: 24, expose: %i[env config cli]),
|
|
116
|
-
Entry.new(key: "session.idle_exit_minutes", yaml_path: %w[session idle_exit_minutes], type: :float, default: 30.0, expose: %i[env config cli]),
|
|
114
|
+
Entry.new(key: "broadcast.triage_model", yaml_path: %w[broadcast triage_model], type: :string, default: nil, expose: %i[env config], lifecycle: :next_broadcast),
|
|
115
|
+
Entry.new(key: "broadcast.triage_host_ref", yaml_path: %w[broadcast triage_host_ref], type: :string, default: nil, expose: %i[env config], lifecycle: :next_broadcast),
|
|
116
|
+
Entry.new(key: "broadcast.triage_base_url", yaml_path: %w[broadcast triage_base_url], type: :string, default: nil, expose: %i[env config], lifecycle: :next_broadcast),
|
|
117
|
+
Entry.new(key: "broadcast.triage_parallel", yaml_path: %w[broadcast triage_parallel], type: :integer, default: 4, expose: %i[env config], lifecycle: :next_broadcast),
|
|
118
|
+
Entry.new(key: "broadcast.triage_deadline", yaml_path: %w[broadcast triage_deadline], type: :float, default: 20.0, expose: %i[env config], lifecycle: :next_broadcast),
|
|
119
|
+
Entry.new(key: "broadcast.threshold", yaml_path: %w[broadcast threshold], type: :float, default: 0.5, expose: %i[env config], lifecycle: :next_broadcast),
|
|
120
|
+
|
|
121
|
+
Entry.new(key: "session.retention_days", yaml_path: %w[session retention_days], type: :integer, default: 14, expose: %i[env config cli], lifecycle: :next_retention_sweep),
|
|
122
|
+
Entry.new(key: "session.max_count", yaml_path: %w[session max_count], type: :integer, default: 500, expose: %i[env config cli], lifecycle: :next_retention_sweep),
|
|
123
|
+
Entry.new(key: "session.keep_status", yaml_path: %w[session keep_status], type: :string, default: "", expose: %i[env config cli], lifecycle: :next_retention_sweep),
|
|
124
|
+
Entry.new(key: "session.sweep_interval_hours", yaml_path: %w[session sweep_interval_hours], type: :integer, default: 24, expose: %i[env config cli], lifecycle: :next_retention_sweep),
|
|
125
|
+
Entry.new(key: "session.idle_exit_minutes", yaml_path: %w[session idle_exit_minutes], type: :float, default: 30.0, expose: %i[env config cli], lifecycle: :worker_start),
|
|
117
126
|
# Plain `chi` runs like `chi --shared` (bin/chi, LaunchMode); false, or --no-shared per run, keeps the plain REPL. No CLI flag: that would duplicate --shared.
|
|
118
|
-
Entry.new(key: "session.shared", yaml_path: %w[session shared], type: :bool, default: true, expose: %i[env config]),
|
|
127
|
+
Entry.new(key: "session.shared", yaml_path: %w[session shared], type: :bool, default: true, expose: %i[env config], lifecycle: :next_invocation),
|
|
119
128
|
# false: a session nothing happened in is deleted when it is left (SessionManager.discardable?).
|
|
120
|
-
Entry.new(key: "session.keep_empty", yaml_path: %w[session keep_empty], type: :bool, default: false, expose: %i[env config]),
|
|
129
|
+
Entry.new(key: "session.keep_empty", yaml_path: %w[session keep_empty], type: :bool, default: false, expose: %i[env config], lifecycle: :next_lifecycle_decision),
|
|
121
130
|
# The most sessions one session may have delegated and still running (the delegate tool); a guard against a runaway model.
|
|
122
|
-
Entry.new(key: "session.max_children", yaml_path: %w[session max_children], type: :integer, default: 4, expose: %i[env config]),
|
|
131
|
+
Entry.new(key: "session.max_children", yaml_path: %w[session max_children], type: :integer, default: 4, expose: %i[env config], lifecycle: :next_lifecycle_decision),
|
|
123
132
|
# A delegate child's turn end reaches its parent (ChildReports): wake runs an idle parent's turn for it, queue waits
|
|
124
133
|
# for the parent's next turn, off is the old behaviour (only delegate_result asks).
|
|
125
|
-
Entry.new(key: "session.delegate_reports", yaml_path: %w[session delegate_reports], type: :enum, default: "wake", expose: %i[env config], enum_values: %w[wake queue off]),
|
|
134
|
+
Entry.new(key: "session.delegate_reports", yaml_path: %w[session delegate_reports], type: :enum, default: "wake", expose: %i[env config], enum_values: %w[wake queue off], lifecycle: :next_lifecycle_decision),
|
|
126
135
|
# Turns a parent runs for delegate reports in a row with no human input; past it reports wait for the next turn.
|
|
127
|
-
Entry.new(key: "session.max_wakes", yaml_path: %w[session max_wakes], type: :integer, default: 10, expose: %i[env config]),
|
|
136
|
+
Entry.new(key: "session.max_wakes", yaml_path: %w[session max_wakes], type: :integer, default: 10, expose: %i[env config], lifecycle: :next_lifecycle_decision),
|
|
128
137
|
|
|
129
138
|
# Images sent to a model (ImageStore): the long side they are downscaled
|
|
130
139
|
# to, the most bytes one may take (bigger → re-encoded as jpeg), and how
|
|
131
140
|
# many one request carries (older ones become placeholders).
|
|
132
|
-
Entry.new(key: "image.max_side", yaml_path: %w[image max_side], type: :integer, default: 1568, expose: %i[env config]),
|
|
133
|
-
Entry.new(key: "image.max_bytes", yaml_path: %w[image max_bytes], type: :integer, default: 3_750_000, expose: %i[env config]),
|
|
134
|
-
Entry.new(key: "image.max_per_request", yaml_path: %w[image max_per_request], type: :integer, default: 20, expose: %i[env config]),
|
|
141
|
+
Entry.new(key: "image.max_side", yaml_path: %w[image max_side], type: :integer, default: 1568, expose: %i[env config], lifecycle: :next_matching_use),
|
|
142
|
+
Entry.new(key: "image.max_bytes", yaml_path: %w[image max_bytes], type: :integer, default: 3_750_000, expose: %i[env config], lifecycle: :next_matching_use),
|
|
143
|
+
Entry.new(key: "image.max_per_request", yaml_path: %w[image max_per_request], type: :integer, default: 20, expose: %i[env config], lifecycle: :next_matching_use),
|
|
135
144
|
|
|
136
145
|
# The guardrails settings are config.yml only: a parent agent must not switch a child's guardrails off from its
|
|
137
146
|
# own environment (SAMAGOTCHI_GUARDRAILS_ENABLED=false). SessionManager.spawn_options unsets them for a worker too.
|
|
138
|
-
Entry.new(key: "guardrails.enabled", yaml_path: %w[guardrails enabled], type: :bool, default: true, expose: %i[config]),
|
|
147
|
+
Entry.new(key: "guardrails.enabled", yaml_path: %w[guardrails enabled], type: :bool, default: true, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
139
148
|
# Which rules vote: auto (the default) leaves out the rules tagged `modes: [strict]`; strict runs them all. An unknown
|
|
140
149
|
# value warns and is auto.
|
|
141
|
-
Entry.new(key: "guardrails.mode", yaml_path: %w[guardrails mode], type: :enum, default: "auto", expose: %i[config], enum_values: %w[auto strict]),
|
|
150
|
+
Entry.new(key: "guardrails.mode", yaml_path: %w[guardrails mode], type: :enum, default: "auto", expose: %i[config], enum_values: %w[auto strict], lifecycle: :next_tool_or_approval),
|
|
142
151
|
# Which models get the rules with `models: small` (Guardrails::ModelSize): auto (32B or less by the name's size, an MoE's
|
|
143
152
|
# active size counting), globs on the bare model name or its key, "|"-separated (a YAML list works too), or "" ([]) for none.
|
|
144
|
-
Entry.new(key: "guardrails.small_models", yaml_path: %w[guardrails small_models], type: :string, default: "auto", expose: %i[config]),
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
Entry.new(key: "
|
|
149
|
-
|
|
153
|
+
Entry.new(key: "guardrails.small_models", yaml_path: %w[guardrails small_models], type: :string, default: "auto", expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
154
|
+
# Which options a parent agent (chi answer) may pick on a session's question, by their effect
|
|
155
|
+
# (Guardrails::ParentPolicy): hard = deny and plain answers; standard = + spend (Continue); soft = + permit (Allow
|
|
156
|
+
# once). An invalid value is hard, the strictest.
|
|
157
|
+
Entry.new(key: "guardrails.parent_answers", yaml_path: %w[guardrails parent_answers], type: :enum, default: "soft", expose: %i[config], enum_values: %w[hard standard soft], invalid: "hard", lifecycle: :next_tool_or_approval),
|
|
158
|
+
|
|
159
|
+
Entry.new(key: "log.file", yaml_path: %w[log file], type: :string, default: nil, expose: %i[env config cli], lifecycle: :process_start),
|
|
160
|
+
Entry.new(key: "log.disable", yaml_path: %w[log disable], type: :bool, default: false, expose: %i[env config cli], lifecycle: :process_start),
|
|
150
161
|
# debug adds payload dumps (model responses, tool args/results) and fetch lines.
|
|
151
|
-
Entry.new(key: "log.level", yaml_path: %w[log level], type: :enum, default: "info", expose: %i[env config cli], enum_values: %w[debug info warn error]),
|
|
162
|
+
Entry.new(key: "log.level", yaml_path: %w[log level], type: :enum, default: "info", expose: %i[env config cli], enum_values: %w[debug info warn error], lifecycle: :process_start),
|
|
152
163
|
|
|
153
|
-
Entry.new(key: "status.line", yaml_path: %w[status line], type: :string, default: "on", expose: %i[env config cli]),
|
|
164
|
+
Entry.new(key: "status.line", yaml_path: %w[status line], type: :string, default: "on", expose: %i[env config cli], lifecycle: :next_matching_turn_display_poll_write),
|
|
154
165
|
|
|
155
|
-
Entry.new(key: "context.status", yaml_path: %w[context status], type: :bool, default: true, expose: %i[env config cli]),
|
|
156
|
-
Entry.new(key: "context.window_tokens", yaml_path: %w[context window_tokens], type: :integer, default: nil, expose: %i[env config cli]),
|
|
157
|
-
Entry.new(key: "context.chars_per_token", yaml_path: %w[context chars_per_token], type: :float, default: 4.0, expose: %i[env config cli]),
|
|
166
|
+
Entry.new(key: "context.status", yaml_path: %w[context status], type: :bool, default: true, expose: %i[env config cli], lifecycle: :next_matching_turn_display_poll_write),
|
|
167
|
+
Entry.new(key: "context.window_tokens", yaml_path: %w[context window_tokens], type: :integer, default: nil, expose: %i[env config cli], lifecycle: :next_matching_turn_display_poll_write),
|
|
168
|
+
Entry.new(key: "context.chars_per_token", yaml_path: %w[context chars_per_token], type: :float, default: 4.0, expose: %i[env config cli], lifecycle: :next_matching_turn_display_poll_write),
|
|
158
169
|
# The tokens one scope's memory index (sent with every prompt) may hold before a write that crosses it gets a note
|
|
159
170
|
# (MemoryBundle::IndexSize); 0: no note. `memory:` is its own section, not the `memories:` preload list.
|
|
160
|
-
Entry.new(key: "memory.index_warn_tokens", yaml_path: %w[memory index_warn_tokens], type: :integer, default: 2500, expose: %i[env config cli]),
|
|
161
|
-
Entry.new(key: "context.status_thresholds", yaml_path: %w[context status_thresholds], type: :string, default: "20,40,60,80", expose: %i[env config cli]),
|
|
171
|
+
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),
|
|
172
|
+
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),
|
|
162
173
|
# Attached context (ContextSources): how often a source's command runs when it names no --every (seconds; less
|
|
163
174
|
# than 30 counts as 30).
|
|
164
|
-
Entry.new(key: "context.every_seconds", yaml_path: %w[context every_seconds], type: :integer, default: 300, expose: %i[env config]),
|
|
175
|
+
Entry.new(key: "context.every_seconds", yaml_path: %w[context every_seconds], type: :integer, default: 300, expose: %i[env config], lifecycle: :next_context_poll),
|
|
165
176
|
# Whether a source's update with wake: true may start a turn in a live, idle session (Worker; one per source per
|
|
166
177
|
# 10 minutes, a peer source per context.peer_turns, within session.max_wakes); false: it waits as a note for the
|
|
167
178
|
# next turn.
|
|
168
|
-
Entry.new(key: "context.wake", yaml_path: %w[context wake], type: :bool, default: true, expose: %i[env config cli]),
|
|
179
|
+
Entry.new(key: "context.wake", yaml_path: %w[context wake], type: :bool, default: true, expose: %i[env config cli], lifecycle: :next_matching_turn_display_poll_write),
|
|
169
180
|
# A peer agent's answers (a source a peer bundle declares, claude-outbox): wake turns in a row they may start
|
|
170
181
|
# with no input typed in the web or a TUI (within session.max_wakes; an agreeing answer's closing wake isn't
|
|
171
182
|
# counted here); then they wait as notes. 0: a peer source
|
|
172
183
|
# wakes like any other (once per 10 minutes, the guard line).
|
|
173
|
-
Entry.new(key: "context.peer_turns", yaml_path: %w[context peer_turns], type: :integer, default: 3, expose: %i[env config cli]),
|
|
184
|
+
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),
|
|
174
185
|
|
|
175
186
|
# What the model is sent of the conversation (LLMContextStrategy, LLMContextView): none, or a list of layers
|
|
176
187
|
# (stale, forget; "|"-separated or a YAML list); forget (the model's forget_outputs) is experimental.
|
|
177
188
|
# models.<key>.llm_context_strategy and hosts.<name>.llm_context_strategy come first.
|
|
178
|
-
Entry.new(key: "llm_context.strategy", yaml_path: %w[llm_context strategy], type: :string, default: "none", expose: %i[env config]),
|
|
189
|
+
Entry.new(key: "llm_context.strategy", yaml_path: %w[llm_context strategy], type: :string, default: "none", expose: %i[env config], lifecycle: :next_matching_turn_display_poll_write),
|
|
179
190
|
# When a layer's edits reach the prompt (LLMContextApply): payoff, next_request or turn_end;
|
|
180
191
|
# models.<key>.llm_context_apply and hosts.<name>.llm_context_apply come first.
|
|
181
|
-
Entry.new(key: "llm_context.apply", yaml_path: %w[llm_context apply], type: :string, default: "payoff", expose: %i[env config]),
|
|
192
|
+
Entry.new(key: "llm_context.apply", yaml_path: %w[llm_context apply], type: :string, default: "payoff", expose: %i[env config], lifecycle: :next_matching_turn_display_poll_write),
|
|
182
193
|
# Whether an edit or write of a file makes its earlier reads stale too (opt-in, experimental).
|
|
183
|
-
Entry.new(key: "llm_context.stale_edits", yaml_path: %w[llm_context stale_edits], type: :bool, default: false, expose: %i[env config]),
|
|
194
|
+
Entry.new(key: "llm_context.stale_edits", yaml_path: %w[llm_context stale_edits], type: :bool, default: false, expose: %i[env config], lifecycle: :next_matching_turn_display_poll_write),
|
|
184
195
|
# The steps (tool batches) back whose edited files' reads stale never stubs.
|
|
185
|
-
Entry.new(key: "llm_context.protect_steps", yaml_path: %w[llm_context protect_steps], type: :integer, default: 3, expose: %i[env config]),
|
|
196
|
+
Entry.new(key: "llm_context.protect_steps", yaml_path: %w[llm_context protect_steps], type: :integer, default: 3, expose: %i[env config], lifecycle: :next_matching_turn_display_poll_write),
|
|
186
197
|
# A soft context budget in tokens (64000, 64k; 4k to 10M): the context status bands (and the forget layer's
|
|
187
198
|
# offers) count against it instead of the window; unset, 0 or off: off. models.<key>.llm_context_budget_tokens
|
|
188
199
|
# and hosts.<name>.llm_context_budget_tokens come first. Kept as written: LLMContextStrategy.parse_budget reads it.
|
|
189
|
-
Entry.new(key: "llm_context.budget_tokens", yaml_path: %w[llm_context budget_tokens], type: :string, default: nil, expose: %i[env config]),
|
|
200
|
+
Entry.new(key: "llm_context.budget_tokens", yaml_path: %w[llm_context budget_tokens], type: :string, default: nil, expose: %i[env config], lifecycle: :next_matching_turn_display_poll_write),
|
|
190
201
|
# The policy sentence forget_outputs' description carries (the forget layer); blank: none.
|
|
191
|
-
Entry.new(key: "llm_context.policy", yaml_path: %w[llm_context policy], type: :string, default: LLMContextStrategy::DEFAULT_POLICY, expose: %i[env config]),
|
|
202
|
+
Entry.new(key: "llm_context.policy", yaml_path: %w[llm_context policy], type: :string, default: LLMContextStrategy::DEFAULT_POLICY, expose: %i[env config], lifecycle: :worker_start),
|
|
192
203
|
|
|
193
|
-
Entry.new(key: "thinking.turn_preamble", yaml_path: %w[thinking turn_preamble], type: :bool, default: true, expose: %i[env config cli]),
|
|
204
|
+
Entry.new(key: "thinking.turn_preamble", yaml_path: %w[thinking turn_preamble], type: :bool, default: true, expose: %i[env config cli], lifecycle: :prompt_rebuild),
|
|
194
205
|
# How much models think: off|low|medium|high|default, parsed by Thinking
|
|
195
206
|
# (a :string, since YAML reads an unquoted off as false). The CLI flag
|
|
196
207
|
# is bin/chi's own --thinking; models: and hosts: entries rank below
|
|
197
208
|
# the flag and the env, above config.yml's value.
|
|
198
|
-
Entry.new(key: "thinking.level", yaml_path: %w[thinking level], type: :string, default: "default", expose: %i[env config]),
|
|
209
|
+
Entry.new(key: "thinking.level", yaml_path: %w[thinking level], type: :string, default: "default", expose: %i[env config], lifecycle: :next_turn_start),
|
|
199
210
|
# Which of its reasoning a chat host's model gets back (ReasoningHistory):
|
|
200
211
|
# the environment only, for A/B runs; models: and hosts: entries are
|
|
201
212
|
# the config surface.
|
|
@@ -204,94 +215,91 @@ module Samagotchi
|
|
|
204
215
|
# The toolkit a new session is stored on (Toolkit.configured): the tools
|
|
205
216
|
# its prompts declare. full (every built-in) when unset; child: a
|
|
206
217
|
# delegate child's. toolkit.kits (the user's own) is read by Toolkit.kits.
|
|
207
|
-
Entry.new(key: "toolkit.default", yaml_path: %w[toolkit default], type: :string, default: nil, expose: %i[env config]),
|
|
208
|
-
Entry.new(key: "toolkit.child", yaml_path: %w[toolkit child], type: :string, default: nil, expose: %i[env config]),
|
|
218
|
+
Entry.new(key: "toolkit.default", yaml_path: %w[toolkit default], type: :string, default: nil, expose: %i[env config], lifecycle: :session_creation),
|
|
219
|
+
Entry.new(key: "toolkit.child", yaml_path: %w[toolkit child], type: :string, default: nil, expose: %i[env config], lifecycle: :session_creation),
|
|
209
220
|
|
|
210
|
-
Entry.new(key: "default.max_tokens", yaml_path: %w[default max_tokens], type: :integer, default: nil, expose: %i[env config cli]),
|
|
211
|
-
Entry.new(key: "max_tool_output_chars", yaml_path: %w[max_tool_output_chars], type: :integer, default: 10_000, expose: %i[env config cli]),
|
|
221
|
+
Entry.new(key: "default.max_tokens", yaml_path: %w[default max_tokens], type: :integer, default: nil, expose: %i[env config cli], lifecycle: :next_matching_use),
|
|
222
|
+
Entry.new(key: "max_tool_output_chars", yaml_path: %w[max_tool_output_chars], type: :integer, default: 10_000, expose: %i[env config cli], lifecycle: :next_matching_use),
|
|
212
223
|
|
|
213
224
|
# The turn-end warm-up (PromptWarmup): auto prefills the next turn's
|
|
214
225
|
# prompt on a local llama.cpp host while the user reads; off never
|
|
215
226
|
# does. A :string, since YAML reads an unquoted off as false.
|
|
216
|
-
Entry.new(key: "cache.warmup", yaml_path: %w[cache warmup], type: :string, default: "auto", expose: %i[env config]),
|
|
227
|
+
Entry.new(key: "cache.warmup", yaml_path: %w[cache warmup], type: :string, default: "auto", expose: %i[env config], lifecycle: :next_request_or_turn_end),
|
|
217
228
|
# How long Anthropic keeps a cache breakpoint (PromptCache): 5m, or
|
|
218
229
|
# 1h for long reading gaps (a 1h write costs 2x the input price, a 5m
|
|
219
230
|
# one 1.25x, so it's opt-in).
|
|
220
|
-
Entry.new(key: "cache.ttl", yaml_path: %w[cache ttl], type: :string, default: "5m", expose: %i[env config]),
|
|
231
|
+
Entry.new(key: "cache.ttl", yaml_path: %w[cache ttl], type: :string, default: "5m", expose: %i[env config], lifecycle: :next_request_or_turn_end),
|
|
221
232
|
# session sends the session id as `prompt_cache_key` to OpenAI's API
|
|
222
233
|
# and OpenRouter (OpenAIChat), which route and cache by it; off sends
|
|
223
234
|
# none. A :string, since YAML reads an unquoted off as false.
|
|
224
|
-
Entry.new(key: "cache.key", yaml_path: %w[cache key], type: :string, default: "off", expose: %i[env config]),
|
|
235
|
+
Entry.new(key: "cache.key", yaml_path: %w[cache key], type: :string, default: "off", expose: %i[env config], lifecycle: :next_request_or_turn_end),
|
|
225
236
|
|
|
226
|
-
Entry.new(key: "retry.max", yaml_path: %w[retry max], type: :integer, default: 5, expose: %i[env config cli]),
|
|
227
|
-
Entry.new(key: "retry.base_delay", yaml_path: %w[retry base_delay], type: :float, default: 0.5, expose: %i[env config cli]),
|
|
228
|
-
Entry.new(key: "retry.max_delay", yaml_path: %w[retry max_delay], type: :float, default: 8.0, expose: %i[env config cli]),
|
|
237
|
+
Entry.new(key: "retry.max", yaml_path: %w[retry max], type: :integer, default: 5, expose: %i[env config cli], lifecycle: :worker_start),
|
|
238
|
+
Entry.new(key: "retry.base_delay", yaml_path: %w[retry base_delay], type: :float, default: 0.5, expose: %i[env config cli], lifecycle: :worker_start),
|
|
239
|
+
Entry.new(key: "retry.max_delay", yaml_path: %w[retry max_delay], type: :float, default: 8.0, expose: %i[env config cli], lifecycle: :worker_start),
|
|
229
240
|
# Asks again in the same turn after an empty answer (EmptyAnswerRetry);
|
|
230
241
|
# 0 = off, capped at 3. No CLI flag: workers get no CLI args.
|
|
231
|
-
Entry.new(key: "retry.empty_answer", yaml_path: %w[retry empty_answer], type: :integer, default: 1, expose: %i[env config]),
|
|
242
|
+
Entry.new(key: "retry.empty_answer", yaml_path: %w[retry empty_answer], type: :integer, default: 1, expose: %i[env config], lifecycle: :next_empty_answer_retry),
|
|
232
243
|
# Seconds a generation must have streamed only thinking before a message
|
|
233
244
|
# for the running turn (the user's, chi send's, a parent agent's) cuts
|
|
234
245
|
# it (Engine#cut_for_steer); 0 = never. hosts.<name>.steer_cut_after and
|
|
235
246
|
# models.<key>.steer_cut_after win. No CLI flag: workers get no CLI args.
|
|
236
|
-
Entry.new(key: "steer.cut_after", yaml_path: %w[steer cut_after], type: :integer, default: 20, expose: %i[env config]),
|
|
247
|
+
Entry.new(key: "steer.cut_after", yaml_path: %w[steer cut_after], type: :integer, default: 20, expose: %i[env config], lifecycle: :next_matching_turn_display_poll_write),
|
|
237
248
|
# A turn's step limit: model ↔ tool rounds before it stops and offers to continue (IterationLimit); integer ≥ 1.
|
|
238
249
|
# --no-interrupt turns get max(1000, this). No CLI flag: --no-interrupt is the flag.
|
|
239
|
-
Entry.new(key: "turn.max_iterations", yaml_path: %w[turn max_iterations], type: :integer, default: 300, expose: %i[env config]),
|
|
240
|
-
# Whether a parent agent (chi answer) may answer Continue to a session's step-limit question; false: Stop only.
|
|
241
|
-
# config.yml only, like guardrails.parent_approvals: a parent must not decide it from its own environment.
|
|
242
|
-
Entry.new(key: "turn.parent_continue", yaml_path: %w[turn parent_continue], type: :bool, default: true, expose: %i[config]),
|
|
250
|
+
Entry.new(key: "turn.max_iterations", yaml_path: %w[turn max_iterations], type: :integer, default: 300, expose: %i[env config], lifecycle: :next_turn_start),
|
|
243
251
|
# What `chi update` touches; its --no-gem/--no-bundles/--no-desktop turn one off for a run.
|
|
244
|
-
Entry.new(key: "update.gem", yaml_path: %w[update gem], type: :bool, default: true, expose: %i[env config]),
|
|
245
|
-
Entry.new(key: "update.bundles", yaml_path: %w[update bundles], type: :bool, default: true, expose: %i[env config]),
|
|
246
|
-
Entry.new(key: "update.desktop", yaml_path: %w[update desktop], type: :bool, default: true, expose: %i[env config]),
|
|
252
|
+
Entry.new(key: "update.gem", yaml_path: %w[update gem], type: :bool, default: true, expose: %i[env config], lifecycle: :next_update),
|
|
253
|
+
Entry.new(key: "update.bundles", yaml_path: %w[update bundles], type: :bool, default: true, expose: %i[env config], lifecycle: :next_update),
|
|
254
|
+
Entry.new(key: "update.desktop", yaml_path: %w[update desktop], type: :bool, default: true, expose: %i[env config], lifecycle: :next_update),
|
|
247
255
|
# The desktop helper's kitty targets (Desktop::MacOS.kitty_settings, one of its terminal_settings): listen_on copied from kitty.conf (unset = none),
|
|
248
256
|
# the kitty binary, and the foreground programs listed, "|"-separated (a YAML list works too; "*" = every window).
|
|
249
|
-
Entry.new(key: "kitty.listen_on", yaml_path: %w[kitty listen_on], type: :string, default: nil, expose: %i[env config]),
|
|
250
|
-
Entry.new(key: "kitty.binary", yaml_path: %w[kitty binary], type: :string, default: "/Applications/kitty.app/Contents/MacOS/kitty", expose: %i[env config]),
|
|
251
|
-
Entry.new(key: "kitty.agents", yaml_path: %w[kitty agents], type: :string, default: DESKTOP_AGENTS, expose: %i[env config]),
|
|
257
|
+
Entry.new(key: "kitty.listen_on", yaml_path: %w[kitty listen_on], type: :string, default: nil, expose: %i[env config], lifecycle: :desktop_update),
|
|
258
|
+
Entry.new(key: "kitty.binary", yaml_path: %w[kitty binary], type: :string, default: "/Applications/kitty.app/Contents/MacOS/kitty", expose: %i[env config], lifecycle: :desktop_update),
|
|
259
|
+
Entry.new(key: "kitty.agents", yaml_path: %w[kitty agents], type: :string, default: DESKTOP_AGENTS, expose: %i[env config], lifecycle: :desktop_update),
|
|
252
260
|
# Its tmux targets (Desktop::MacOS.tmux_settings): off until enabled; the tmux binary (unset = the tmux on
|
|
253
261
|
# chi's PATH when the launch file is written) and the foreground programs listed, as kitty.agents.
|
|
254
|
-
Entry.new(key: "tmux.enabled", yaml_path: %w[tmux enabled], type: :bool, default: false, expose: %i[env config]),
|
|
255
|
-
Entry.new(key: "tmux.binary", yaml_path: %w[tmux binary], type: :string, default: nil, expose: %i[env config]),
|
|
256
|
-
Entry.new(key: "tmux.agents", yaml_path: %w[tmux agents], type: :string, default: DESKTOP_AGENTS, expose: %i[env config]),
|
|
262
|
+
Entry.new(key: "tmux.enabled", yaml_path: %w[tmux enabled], type: :bool, default: false, expose: %i[env config], lifecycle: :desktop_update),
|
|
263
|
+
Entry.new(key: "tmux.binary", yaml_path: %w[tmux binary], type: :string, default: nil, expose: %i[env config], lifecycle: :desktop_update),
|
|
264
|
+
Entry.new(key: "tmux.agents", yaml_path: %w[tmux agents], type: :string, default: DESKTOP_AGENTS, expose: %i[env config], lifecycle: :desktop_update),
|
|
257
265
|
|
|
258
|
-
Entry.new(key: "read.truncate_at_bytes", yaml_path: %w[read truncate_at_bytes], type: :integer, default: 65_536, expose: %i[env config cli]),
|
|
259
|
-
Entry.new(key: "read.preview_bytes", yaml_path: %w[read preview_bytes], type: :integer, default: 12_288, expose: %i[env config cli]),
|
|
260
|
-
Entry.new(key: "read.hard_max_bytes", yaml_path: %w[read hard_max_bytes], type: :integer, default: 2_097_152, expose: %i[env config cli]),
|
|
261
|
-
Entry.new(key: "read.telemetry_threshold_pct", yaml_path: %w[read telemetry_threshold_pct], type: :integer, default: 80, expose: %i[env config cli]),
|
|
266
|
+
Entry.new(key: "read.truncate_at_bytes", yaml_path: %w[read truncate_at_bytes], type: :integer, default: 65_536, expose: %i[env config cli], lifecycle: :next_matching_use),
|
|
267
|
+
Entry.new(key: "read.preview_bytes", yaml_path: %w[read preview_bytes], type: :integer, default: 12_288, expose: %i[env config cli], lifecycle: :next_matching_use),
|
|
268
|
+
Entry.new(key: "read.hard_max_bytes", yaml_path: %w[read hard_max_bytes], type: :integer, default: 2_097_152, expose: %i[env config cli], lifecycle: :next_matching_use),
|
|
269
|
+
Entry.new(key: "read.telemetry_threshold_pct", yaml_path: %w[read telemetry_threshold_pct], type: :integer, default: 80, expose: %i[env config cli], lifecycle: :next_matching_use),
|
|
262
270
|
|
|
263
|
-
Entry.new(key: "execute.truncate_at_bytes", yaml_path: %w[execute truncate_at_bytes], type: :integer, default: 65_536, expose: %i[env config cli]),
|
|
264
|
-
Entry.new(key: "execute.preview_bytes", yaml_path: %w[execute preview_bytes], type: :integer, default: 12_288, expose: %i[env config cli]),
|
|
271
|
+
Entry.new(key: "execute.truncate_at_bytes", yaml_path: %w[execute truncate_at_bytes], type: :integer, default: 65_536, expose: %i[env config cli], lifecycle: :next_matching_use),
|
|
272
|
+
Entry.new(key: "execute.preview_bytes", yaml_path: %w[execute preview_bytes], type: :integer, default: 12_288, expose: %i[env config cli], lifecycle: :next_matching_use),
|
|
265
273
|
# The head an execute result over max_tool_output_chars keeps (ToolOutputCut): at most this, and a quarter of
|
|
266
274
|
# the room; the tail gets the rest.
|
|
267
|
-
Entry.new(key: "execute.cut_head_chars", yaml_path: %w[execute cut_head_chars], type: :integer, default: 1000, expose: %i[env config cli]),
|
|
268
|
-
Entry.new(key: "execute.telemetry_threshold_pct", yaml_path: %w[execute telemetry_threshold_pct], type: :integer, default: 80, expose: %i[env config cli]),
|
|
275
|
+
Entry.new(key: "execute.cut_head_chars", yaml_path: %w[execute cut_head_chars], type: :integer, default: 1000, expose: %i[env config cli], lifecycle: :next_matching_use),
|
|
276
|
+
Entry.new(key: "execute.telemetry_threshold_pct", yaml_path: %w[execute telemetry_threshold_pct], type: :integer, default: 80, expose: %i[env config cli], lifecycle: :next_matching_use),
|
|
269
277
|
# Seconds one execute command may run before execute.on_timeout applies (Tools::Execute::TIMEOUT_SEC).
|
|
270
|
-
Entry.new(key: "execute.timeout_sec", yaml_path: %w[execute timeout_sec], type: :integer, default: 120, expose: %i[env config cli]),
|
|
278
|
+
Entry.new(key: "execute.timeout_sec", yaml_path: %w[execute timeout_sec], type: :integer, default: 120, expose: %i[env config cli], lifecycle: :next_matching_use),
|
|
271
279
|
# What happens to a command still running at that limit (Tools::Execute::ON_TIMEOUT): task hands it over as a
|
|
272
280
|
# background task (task_wait waits for it), kill stops it.
|
|
273
|
-
Entry.new(key: "execute.on_timeout", yaml_path: %w[execute on_timeout], type: :enum, default: "task", expose: %i[env config cli], enum_values: %w[task kill]),
|
|
281
|
+
Entry.new(key: "execute.on_timeout", yaml_path: %w[execute on_timeout], type: :enum, default: "task", expose: %i[env config cli], enum_values: %w[task kill], lifecycle: :next_matching_use),
|
|
274
282
|
# Whether execute declares its optional description parameter (a few model-written words, a tool row's title).
|
|
275
|
-
Entry.new(key: "execute.description", yaml_path: %w[execute description], type: :bool, default: true, expose: %i[env config]),
|
|
283
|
+
Entry.new(key: "execute.description", yaml_path: %w[execute description], type: :bool, default: true, expose: %i[env config], lifecycle: :worker_start),
|
|
276
284
|
|
|
277
|
-
Entry.new(key: "web.port", yaml_path: %w[web port], type: :integer, default: 4567, expose: %i[env config cli]),
|
|
278
|
-
Entry.new(key: "web.host", yaml_path: %w[web host], type: :string, default: "127.0.0.1", expose: %i[env config cli]),
|
|
279
|
-
Entry.new(key: "web.markdown", yaml_path: %w[web markdown], type: :bool, default: false, expose: %i[env config cli]),
|
|
285
|
+
Entry.new(key: "web.port", yaml_path: %w[web port], type: :integer, default: 4567, expose: %i[env config cli], lifecycle: :web_start),
|
|
286
|
+
Entry.new(key: "web.host", yaml_path: %w[web host], type: :string, default: "127.0.0.1", expose: %i[env config cli], lifecycle: :web_start),
|
|
287
|
+
Entry.new(key: "web.markdown", yaml_path: %w[web markdown], type: :bool, default: false, expose: %i[env config cli], lifecycle: :web_start),
|
|
280
288
|
# The page's view of a turn: stage (the running turn pinned above the composer) or turn (one block of generations, the live one at the bottom); ?view= overrides it per page load.
|
|
281
|
-
Entry.new(key: "web.view", yaml_path: %w[web view], type: :enum, default: "stage", expose: %i[env config cli], enum_values: %w[stage turn]),
|
|
289
|
+
Entry.new(key: "web.view", yaml_path: %w[web view], type: :enum, default: "stage", expose: %i[env config cli], enum_values: %w[stage turn], lifecycle: :web_start),
|
|
282
290
|
# Quick replies next to Annotate in the page's selection bubble, "|"-separated (a YAML list works too); "" leaves only Annotate.
|
|
283
|
-
Entry.new(key: "web.annotate_presets", yaml_path: %w[web annotate_presets], type: :string, default: "Agreed|Could you please elaborate?", expose: %i[env config cli]),
|
|
291
|
+
Entry.new(key: "web.annotate_presets", yaml_path: %w[web annotate_presets], type: :string, default: "Agreed|Could you please elaborate?", expose: %i[env config cli], lifecycle: :web_start),
|
|
284
292
|
# What a file ref (a tool row's file, a diff line) opens on this machine: a preset (vscode, vscode-insiders, cursor, zed, none) or a URL template with {path} and an optional {line} (Web::Editor).
|
|
285
|
-
Entry.new(key: "web.editor", yaml_path: %w[web editor], type: :string, default: "vscode", expose: %i[env config cli]),
|
|
293
|
+
Entry.new(key: "web.editor", yaml_path: %w[web editor], type: :string, default: "vscode", expose: %i[env config cli], lifecycle: :web_start),
|
|
286
294
|
|
|
287
|
-
Entry.new(key: "no_interrupt", yaml_path: %w[no_interrupt], type: :bool, default: false, expose: %i[env config cli]),
|
|
288
|
-
Entry.new(key: "no_default_input", yaml_path: %w[no_default_input], type: :bool, default: false, expose: %i[env config cli]),
|
|
295
|
+
Entry.new(key: "no_interrupt", yaml_path: %w[no_interrupt], type: :bool, default: false, expose: %i[env config cli], lifecycle: :next_cli_run),
|
|
296
|
+
Entry.new(key: "no_default_input", yaml_path: %w[no_default_input], type: :bool, default: false, expose: %i[env config cli], lifecycle: :next_cli_run),
|
|
289
297
|
|
|
290
298
|
# env+config only
|
|
291
|
-
Entry.new(key: "default.input", yaml_path: %w[default input], type: :string, default: nil, expose: %i[env config]),
|
|
292
|
-
Entry.new(key: "history.file", yaml_path: %w[history file], type: :string, default: nil, expose: %i[env config]),
|
|
293
|
-
Entry.new(key: "skip_agents_md", yaml_path: %w[skip_agents_md], type: :bool, default: false, expose: %i[env config])
|
|
294
|
-
].freeze
|
|
299
|
+
Entry.new(key: "default.input", yaml_path: %w[default input], type: :string, default: nil, expose: %i[env config], lifecycle: :next_cli_run),
|
|
300
|
+
Entry.new(key: "history.file", yaml_path: %w[history file], type: :string, default: nil, expose: %i[env config], lifecycle: :next_history_access),
|
|
301
|
+
Entry.new(key: "skip_agents_md", yaml_path: %w[skip_agents_md], type: :bool, default: false, expose: %i[env config], lifecycle: :prompt_rebuild)
|
|
302
|
+
].each(&:freeze).freeze
|
|
295
303
|
|
|
296
304
|
# Top-level maps and lists read by their own code, whose entry names and
|
|
297
305
|
# contents are the user's: ConfigFile.model_aliases / preloaded_memories /
|
|
@@ -373,8 +381,9 @@ module Samagotchi
|
|
|
373
381
|
lowered = str.downcase
|
|
374
382
|
allowed = entry.enum_values.map(&:downcase)
|
|
375
383
|
unless allowed.include?(lowered)
|
|
376
|
-
|
|
377
|
-
|
|
384
|
+
using = entry.invalid ? "using #{entry.invalid}" : "using default"
|
|
385
|
+
Log.warn(:config, "invalid_value", echo: "Warning: invalid value for #{entry.key}: #{raw.inspect} (allowed: #{entry.enum_values.join(", ")}) — #{using}", key: entry.key)
|
|
386
|
+
return entry.invalid || entry.default
|
|
378
387
|
end
|
|
379
388
|
# return canonical casing from enum_values
|
|
380
389
|
entry.enum_values.find { |v| v.downcase == lowered }
|
|
@@ -626,9 +635,18 @@ module Samagotchi
|
|
|
626
635
|
@known_sections ||= known_config_keys.filter_map { |k| k.split(".").first if k.include?(".") }.to_set.freeze
|
|
627
636
|
end
|
|
628
637
|
|
|
638
|
+
# Keys replaced by another: the warning names the new one (no alias).
|
|
639
|
+
REPLACED_KEYS = {
|
|
640
|
+
"guardrails.parent_approvals" => "guardrails.parent_answers",
|
|
641
|
+
"turn.parent_continue" => "guardrails.parent_answers"
|
|
642
|
+
}.freeze
|
|
643
|
+
|
|
629
644
|
# nil when +dotted+ is a key the file may hold.
|
|
630
645
|
def key_problem(dotted)
|
|
631
646
|
return nil if known_config_keys.include?(dotted)
|
|
647
|
+
if (replaced = REPLACED_KEYS[dotted])
|
|
648
|
+
return "config: unknown key '#{dotted}': replaced by '#{replaced}' (hard, standard or soft; see docs/guardrails.md)"
|
|
649
|
+
end
|
|
632
650
|
|
|
633
651
|
entry = find_by_key(dotted)
|
|
634
652
|
if entry
|
|
@@ -729,6 +747,7 @@ module Samagotchi
|
|
|
729
747
|
cache = @yaml_cache ||= {}
|
|
730
748
|
unless File.file?(path)
|
|
731
749
|
cache.delete(path)
|
|
750
|
+
notify_restart_settings(path, nil, nil)
|
|
732
751
|
return nil
|
|
733
752
|
end
|
|
734
753
|
stat = File.stat(path)
|
|
@@ -743,9 +762,70 @@ module Samagotchi
|
|
|
743
762
|
end
|
|
744
763
|
data = nil unless data.is_a?(Hash)
|
|
745
764
|
cache[path] = [key, data]
|
|
765
|
+
notify_restart_settings(path, key, data)
|
|
746
766
|
data
|
|
747
767
|
end
|
|
748
768
|
|
|
769
|
+
# Calls +on_change+ with one line ("server.port changed — applies after a
|
|
770
|
+
# worker restart") when config.yml changes a setting this process only
|
|
771
|
+
# reads at its start (Config::Entry#lifecycle: worker_start or web_start,
|
|
772
|
+
# and process_start). Checked when the file is read again (#read_yaml),
|
|
773
|
+
# once per key per new value: none at start, none for a value back at its
|
|
774
|
+
# start value.
|
|
775
|
+
# @param process [Symbol] :worker or :web
|
|
776
|
+
# @return [Proc] unsubscribes
|
|
777
|
+
def watch_restart_settings!(process:, env: ENV, &on_change)
|
|
778
|
+
lifecycles = case process
|
|
779
|
+
when :worker then %i[worker_start process_start]
|
|
780
|
+
when :web then %i[web_start process_start]
|
|
781
|
+
else raise ArgumentError, "unknown config process #{process.inspect}"
|
|
782
|
+
end
|
|
783
|
+
raise ArgumentError, "a config change callback is required" unless on_change
|
|
784
|
+
|
|
785
|
+
path = global_path(env: env)
|
|
786
|
+
data = read_yaml(env: env, path: path)
|
|
787
|
+
entries = Config::ENTRIES.select { |entry| entry.config_exposed? && lifecycles.include?(entry.lifecycle) }
|
|
788
|
+
watcher = {
|
|
789
|
+
path: path, process: process, entries: entries,
|
|
790
|
+
values: entries.to_h { |entry| [entry.key, Config.resolve(entry.key, file_data: data, env: {})] },
|
|
791
|
+
noticed: {},
|
|
792
|
+
file_key: File.file?(path) ? [File.stat(path).mtime.to_r, File.size(path)] : nil,
|
|
793
|
+
on_change: on_change
|
|
794
|
+
}
|
|
795
|
+
restart_watchers_mutex.synchronize { (@restart_watchers ||= []) << watcher }
|
|
796
|
+
-> { restart_watchers_mutex.synchronize { @restart_watchers&.delete(watcher) } }
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
# Tells each watcher of +path+ which of its start-only settings the file
|
|
800
|
+
# read now (+data+, under +file_key+) changes.
|
|
801
|
+
def notify_restart_settings(path, file_key, data)
|
|
802
|
+
notices = restart_watchers_mutex.synchronize do
|
|
803
|
+
Array(@restart_watchers).flat_map do |watcher|
|
|
804
|
+
next [] unless watcher[:path] == path && watcher[:file_key] != file_key
|
|
805
|
+
|
|
806
|
+
watcher[:file_key] = file_key
|
|
807
|
+
watcher[:entries].filter_map do |entry|
|
|
808
|
+
value = Config.resolve(entry.key, file_data: data, env: {})
|
|
809
|
+
if watcher[:values][entry.key] == value
|
|
810
|
+
watcher[:noticed].delete(entry.key)
|
|
811
|
+
next
|
|
812
|
+
end
|
|
813
|
+
# Already told for this value: a later edit of another key.
|
|
814
|
+
next if watcher[:noticed].key?(entry.key) && watcher[:noticed][entry.key] == value
|
|
815
|
+
|
|
816
|
+
watcher[:noticed][entry.key] = value
|
|
817
|
+
restart = watcher[:process] == :worker ? "a worker restart" : "restarting chi web"
|
|
818
|
+
[watcher[:on_change], "#{entry.key} changed — applies after #{restart}"]
|
|
819
|
+
end
|
|
820
|
+
end
|
|
821
|
+
end
|
|
822
|
+
notices.each { |callback, message| callback.call(message) }
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def restart_watchers_mutex
|
|
826
|
+
@restart_watchers_mutex ||= Mutex.new
|
|
827
|
+
end
|
|
828
|
+
|
|
749
829
|
# Print +message+ to stderr unless this process already did. The config
|
|
750
830
|
# is read again for every HostRegistry, worker env and model lookup, so a
|
|
751
831
|
# warning about the file would otherwise repeat several times per run.
|
|
@@ -62,7 +62,7 @@ module Samagotchi
|
|
|
62
62
|
"context healthy — proceed normally",
|
|
63
63
|
"context moderate — prefer targeted and range reads over full-file dumps",
|
|
64
64
|
"context elevated — be concise, prefer range reads, avoid re-reading large files",
|
|
65
|
-
"context critical —
|
|
65
|
+
"context critical — write a short handoff note (state, next step), avoid large outputs, delegate broad work to subagents"
|
|
66
66
|
].freeze
|
|
67
67
|
# The top level's words when the buckets count against a budget
|
|
68
68
|
# (llm_context.budget_tokens) and no forget layer: nothing in the
|
|
@@ -195,7 +195,8 @@ module Samagotchi
|
|
|
195
195
|
text += "\nPrompt: #{prompt}"
|
|
196
196
|
end
|
|
197
197
|
{ kind: KIND, header: HEADER, question: text, options: [CONTINUE, STOP], multi_select: false,
|
|
198
|
-
|
|
198
|
+
# Continue spends time and tokens, Stop denies (ParentPolicy).
|
|
199
|
+
allow_freeform: true, effects: %w[spend deny], limit: limit, context: context }.compact
|
|
199
200
|
end
|
|
200
201
|
|
|
201
202
|
# The limit the offer's turn ran out at (Engine's last_turn marker).
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -1518,8 +1518,8 @@ module Samagotchi
|
|
|
1518
1518
|
private :guardrail_notify
|
|
1519
1519
|
|
|
1520
1520
|
# @return [Hash, nil] {selected:, freeform:, selected_indices:}
|
|
1521
|
-
def hook_ask_user(question, options, header, allow_freeform, hook)
|
|
1522
|
-
@hook_routes.ask_user(question, options, header, allow_freeform, hook)
|
|
1521
|
+
def hook_ask_user(question, options, header, allow_freeform, hook, effects: nil)
|
|
1522
|
+
@hook_routes.ask_user(question, options, header, allow_freeform, hook, effects: effects)
|
|
1523
1523
|
end
|
|
1524
1524
|
private :hook_ask_user
|
|
1525
1525
|
|
|
@@ -2553,8 +2553,8 @@ module Samagotchi
|
|
|
2553
2553
|
messages: -> { plugin_messages },
|
|
2554
2554
|
messages_partial: -> { turn_running? && @running_turn_messages.nil? },
|
|
2555
2555
|
notify: ->(text, level, label, fallback_for: nil) { hook_notify(text, level, label, fallback_for: fallback_for) },
|
|
2556
|
-
ask_user: lambda { |question:, options:, header:, allow_freeform:, hook
|
|
2557
|
-
hook_ask_user(question, options, header, allow_freeform, hook)
|
|
2556
|
+
ask_user: lambda { |question:, options:, header:, allow_freeform:, hook:, effects: nil|
|
|
2557
|
+
hook_ask_user(question, options, header, allow_freeform, hook, effects: effects)
|
|
2558
2558
|
},
|
|
2559
2559
|
# An init task's is its own (a Ctrl-C ends a turn, not the task).
|
|
2560
2560
|
cancelled: lambda {
|
|
@@ -5,6 +5,7 @@ require "yaml"
|
|
|
5
5
|
require_relative "config"
|
|
6
6
|
require_relative "log"
|
|
7
7
|
require_relative "guardrails"
|
|
8
|
+
require_relative "guardrails/parent_approvals"
|
|
8
9
|
require_relative "memory_bundle/manifest"
|
|
9
10
|
require_relative "version"
|
|
10
11
|
require_relative "hooks"
|
|
@@ -236,6 +237,9 @@ module Samagotchi
|
|
|
236
237
|
# verdict.call is the call that will run (a hook may have replaced it).
|
|
237
238
|
payload = Guardrails::Approval.payload(verdict, label: label, preview: approval_preview(verdict.call),
|
|
238
239
|
only: (SCRATCH_SCOPES if @scratch))
|
|
240
|
+
# A call on chi's own config, hooks or guardrails is the user's to
|
|
241
|
+
# allow, never a parent agent's (Guardrails::ParentPolicy).
|
|
242
|
+
payload[:user_only] = Guardrails::ParentApprovals.protected_facts?(payload[:approval])
|
|
239
243
|
Guardrails::Approval.settle(verdict, @ask.call(payload), payload[:approval][:scopes])
|
|
240
244
|
end
|
|
241
245
|
|