samagotchi 0.154.0 → 0.155.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 +30 -1
- data/docs/architecture.md +3 -2
- data/docs/bundles.md +21 -0
- data/docs/cli.md +10 -8
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +4 -4
- data/docs/internals/attached-context.md +8 -6
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/bridge.md +18 -5
- data/docs/internals/engine.md +221 -100
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +12 -6
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +3 -3
- data/docs/testing.md +16 -1
- data/lib/samagotchi/answer_text.rb +1 -2
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bridge/card_places.rb +56 -7
- data/lib/samagotchi/bridge/card_store.rb +24 -9
- data/lib/samagotchi/bridge.rb +9 -0
- data/lib/samagotchi/bridge_client.rb +9 -5
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +1 -1
- data/lib/samagotchi/engine/context_judge.rb +19 -125
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/objectives.rb +22 -90
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +5 -2
- data/lib/samagotchi/engine/toolkit.rb +33 -190
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +162 -99
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/engine_snapshots.rb +9 -8
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +6 -4
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/idle_recap.rb +3 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +194 -216
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +66 -42
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/session_manager.rb +8 -13
- data/lib/samagotchi/session_retention.rb +24 -4
- data/lib/samagotchi/steer.rb +12 -1
- data/lib/samagotchi/stream_event.rb +22 -0
- data/lib/samagotchi/system_prompt.rb +84 -61
- data/lib/samagotchi/terminal_ui/attached_view.rb +0 -9
- data/lib/samagotchi/terminal_ui/input_support.rb +16 -15
- data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
- data/lib/samagotchi/terminal_ui/reline_seam.rb +0 -10
- data/lib/samagotchi/terminal_ui/status_row.rb +0 -7
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/tool_activity.rb +8 -6
- data/lib/samagotchi/tool_declarations.rb +4 -4
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +93 -101
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/registry.rb +43 -40
- data/lib/samagotchi/tools/tool_index.rb +135 -88
- data/lib/samagotchi/tools/tool_surface.rb +173 -0
- data/lib/samagotchi/turn_context.rb +67 -0
- data/lib/samagotchi/turn_flow.rb +7 -2
- data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
- data/lib/samagotchi/turn_stages/objectives.rb +146 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
- data/lib/samagotchi/turn_stages/warmup.rb +83 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -1
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +8 -1
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- data/lib/samagotchi/web/public/card_stamps.js +27 -0
- data/lib/samagotchi/web/public/cards_place.js +5 -3
- data/lib/samagotchi/web/public/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/composer_mode.js +1 -1
- data/lib/samagotchi/web/public/format.js +3 -2
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +8 -4
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- data/lib/samagotchi/web/session_access_routes.rb +8 -0
- data/lib/samagotchi/web/session_show.rb +4 -0
- data/lib/samagotchi/worker.rb +8 -5
- data/lib/samagotchi.rb +0 -1
- metadata +19 -2
- data/lib/samagotchi/pending_input_queue.rb +0 -55
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "degrade"
|
|
3
4
|
require_relative "session_setting"
|
|
4
5
|
require_relative "token_usage"
|
|
5
6
|
|
|
@@ -171,16 +172,16 @@ module Samagotchi
|
|
|
171
172
|
# doesn't count. Without a model (EffectiveModel), no probe. A remote
|
|
172
173
|
# chat host has no /props: nil until a turn. The name asked is never
|
|
173
174
|
# resolved (HostRegistry#bare_name), so it holds on a broken resolve.
|
|
174
|
-
def served_model_for(snapshot, model: nil)
|
|
175
|
-
asked = bare_model_name(model ? model.name : effective_model_name)
|
|
176
|
-
return [snapshot[:served_model], asked] if snapshot[:served_model] && snapshot[:served_model_for] == asked
|
|
177
|
-
return [nil, nil] unless model
|
|
178
|
-
|
|
179
|
-
model.served_from_props(asked)
|
|
180
175
|
# Only bare_model_name and the snapshot lookup are left to fail here:
|
|
181
176
|
# the probe's failure is EffectiveModel#served_from_props's.
|
|
182
|
-
|
|
183
|
-
[nil, nil]
|
|
177
|
+
def served_model_for(snapshot, model: nil)
|
|
178
|
+
Degrade.call(:model, "served_model_unreadable", fallback: [nil, nil]) do
|
|
179
|
+
asked = bare_model_name(model ? model.name : effective_model_name)
|
|
180
|
+
return [snapshot[:served_model], asked] if snapshot[:served_model] && snapshot[:served_model_for] == asked
|
|
181
|
+
return [nil, nil] unless model
|
|
182
|
+
|
|
183
|
+
model.served_from_props(asked)
|
|
184
|
+
end
|
|
184
185
|
end
|
|
185
186
|
|
|
186
187
|
# The next turn's kit prompt text for /stats: its kit and its estimated
|
|
@@ -26,7 +26,8 @@ module Samagotchi
|
|
|
26
26
|
# @param thinking_notices [#call] → ThinkingNotices
|
|
27
27
|
# @param metrics [#call] → SessionMetrics
|
|
28
28
|
# @param hooks [#call] → Hooks::Registry
|
|
29
|
-
# @param
|
|
29
|
+
# @param turn_context [#call] → TurnContext, nil: the running turn's (Engine#turn_context)
|
|
30
|
+
# @param last_turn_context [#call] → TurnContext, nil: the last finished turn's
|
|
30
31
|
# @param model_settings [#call] → SessionModelSettings
|
|
31
32
|
# @param session [#call] → Session, nil
|
|
32
33
|
# @param live_conversation [#call] → Array<Hash>, the running turn's conversation
|
|
@@ -35,8 +36,8 @@ module Samagotchi
|
|
|
35
36
|
# @param working_set_strategy [#call] → the strategy the working set is built under
|
|
36
37
|
# @param turn_running [#call] → Boolean
|
|
37
38
|
# @param announce_or_hold [#call] (event) → announces the event, or holds it
|
|
38
|
-
def initialize(observer:, used_memories:, muted:, steer_cut:, thinking_notices:, metrics:, hooks:,
|
|
39
|
-
model_settings:, session:, live_conversation:, used_memory_names:, served_expected_by:,
|
|
39
|
+
def initialize(observer:, used_memories:, muted:, steer_cut:, thinking_notices:, metrics:, hooks:, turn_context:,
|
|
40
|
+
last_turn_context:, model_settings:, session:, live_conversation:, used_memory_names:, served_expected_by:,
|
|
40
41
|
working_set_strategy:, turn_running:, announce_or_hold:)
|
|
41
42
|
@observer = observer
|
|
42
43
|
@used_memories = used_memories
|
|
@@ -45,7 +46,8 @@ module Samagotchi
|
|
|
45
46
|
@thinking_notices = thinking_notices
|
|
46
47
|
@metrics = metrics
|
|
47
48
|
@hooks = hooks
|
|
48
|
-
@
|
|
49
|
+
@turn_context = turn_context
|
|
50
|
+
@last_turn_context = last_turn_context
|
|
49
51
|
@model_settings = model_settings
|
|
50
52
|
@session = session
|
|
51
53
|
@live_conversation = live_conversation
|
|
@@ -149,15 +151,18 @@ module Samagotchi
|
|
|
149
151
|
end
|
|
150
152
|
|
|
151
153
|
# The strategy the set is built under: the running turn's
|
|
152
|
-
# (TurnSettings#llm_context: what its requests send)
|
|
153
|
-
# the
|
|
154
|
-
#
|
|
155
|
-
#
|
|
154
|
+
# (TurnSettings#llm_context: what its requests send), the last turn's
|
|
155
|
+
# while the running one has no settings yet (before Engine#generate);
|
|
156
|
+
# between turns the next turn's (as TurnStages::Warmup#after: the last turn's
|
|
157
|
+
# settings are stale after a /llm-context or /model change), the last
|
|
158
|
+
# turn's when it can't be resolved. One read of each published context
|
|
159
|
+
# (TurnState): any thread may ask (/working-set's anytime list).
|
|
156
160
|
def working_set_llm_context
|
|
157
|
-
|
|
158
|
-
|
|
161
|
+
running = turn_context
|
|
162
|
+
last = last_turn_context&.settings&.llm_context
|
|
163
|
+
return running.settings ? running.settings.llm_context : last if running
|
|
159
164
|
|
|
160
|
-
model_settings.turn_llm_context ||
|
|
165
|
+
model_settings.turn_llm_context || last
|
|
161
166
|
end
|
|
162
167
|
|
|
163
168
|
# A :generation_completed with the generation's decode speed (`speed:`
|
|
@@ -222,7 +227,8 @@ module Samagotchi
|
|
|
222
227
|
def thinking_notices = @thinking_notices.call
|
|
223
228
|
def metrics = @metrics.call
|
|
224
229
|
def hooks = @hooks.call
|
|
225
|
-
def
|
|
230
|
+
def turn_context = @turn_context.call
|
|
231
|
+
def last_turn_context = @last_turn_context.call
|
|
226
232
|
def model_settings = @model_settings.call
|
|
227
233
|
def session = @session.call
|
|
228
234
|
def live_conversation = @live_conversation.call
|
|
@@ -24,8 +24,17 @@ module Samagotchi
|
|
|
24
24
|
# a tool call is judged in, the approval question, and the Gate the kernel
|
|
25
25
|
# asks before every tool call.
|
|
26
26
|
#
|
|
27
|
-
# What it needs from the Engine comes through lookups, read at call time
|
|
27
|
+
# What it needs from the Engine comes through lookups, read at call time;
|
|
28
|
+
# what it needs of a turn (its author, participant mode, git facts,
|
|
29
|
+
# origin) comes with the turn's TurnContext, which the Gate is given per
|
|
30
|
+
# call. It keeps no per-turn state.
|
|
28
31
|
class GuardrailWiring
|
|
32
|
+
# What #begin_turn decides for a turn, once (Engine#begin_turn puts it
|
|
33
|
+
# in the TurnContext): its Guardrails::GitInfo (asked afresh), the
|
|
34
|
+
# participant's Participants::Entry and their turn's mode (nil both for
|
|
35
|
+
# the owner's turns).
|
|
36
|
+
TurnStart = Data.define(:git, :participant, :mode)
|
|
37
|
+
|
|
29
38
|
# @return [Guardrails::LoadFailures] what failed to load (hooks, rules)
|
|
30
39
|
attr_reader :failures
|
|
31
40
|
|
|
@@ -33,11 +42,10 @@ module Samagotchi
|
|
|
33
42
|
attr_reader :approvals
|
|
34
43
|
|
|
35
44
|
# @param scratch [Boolean] a `chi scratch` session (writes into the memories are denied)
|
|
36
|
-
# @param
|
|
37
|
-
#
|
|
45
|
+
# @param tools [#call] → Tools::Registry: an approval's plugin label.
|
|
46
|
+
# A lookup: the Engine builds its wiring (and so the load failures its
|
|
47
|
+
# hooks are loaded into, ExtensionLoad) before its tools and hooks.
|
|
38
48
|
# @param session [#call] → Session, nil
|
|
39
|
-
# @param model_key [#call] → String, the effective model's overlay key
|
|
40
|
-
# @param model_name [#call] → String, nil: the effective model's bare name
|
|
41
49
|
# @param cancelled [#call] → Boolean, whether the running turn is cancelled
|
|
42
50
|
# @param ask [#call] (fields) → the answer; the Engine's question flow
|
|
43
51
|
# @param notify [#call] (message) → shows a guardrails line in every UI
|
|
@@ -50,15 +58,11 @@ module Samagotchi
|
|
|
50
58
|
# @param participant [#call] (author) → the participant's
|
|
51
59
|
# Participants::Entry (participants.json: their name, their grant's
|
|
52
60
|
# mode), nil for none
|
|
53
|
-
def initialize(scratch:,
|
|
54
|
-
|
|
55
|
-
participant: ->(_author) {})
|
|
61
|
+
def initialize(scratch:, tools:, session:, cancelled:, ask:, notify: ->(_message) {}, system_one: -> {},
|
|
62
|
+
system_one_runs: -> {}, cancel_controller: -> {}, participant: ->(_author) {})
|
|
56
63
|
@scratch = scratch
|
|
57
|
-
@hooks_lookup = hooks
|
|
58
64
|
@tools_lookup = tools
|
|
59
65
|
@session_lookup = session
|
|
60
|
-
@model_key_lookup = model_key
|
|
61
|
-
@model_name_lookup = model_name
|
|
62
66
|
@cancelled_lookup = cancelled
|
|
63
67
|
@ask = ask
|
|
64
68
|
@notify = notify
|
|
@@ -66,15 +70,12 @@ module Samagotchi
|
|
|
66
70
|
@system_one_runs = system_one_runs
|
|
67
71
|
@cancel_controller = cancel_controller
|
|
68
72
|
@participant = participant
|
|
69
|
-
@turn_mode = nil
|
|
70
73
|
# Bundle name → the rules last loaded from it whole, kept while the
|
|
71
74
|
# installed version needs a newer chi; [name, version] said so once.
|
|
72
75
|
@loaded_bundle_rules = {}
|
|
73
76
|
@kept_noticed = {}
|
|
74
77
|
@failures = Guardrails::LoadFailures.new
|
|
75
78
|
@rules_mutex = Mutex.new
|
|
76
|
-
@git = Guardrails::GitInfo.new
|
|
77
|
-
@origin = TurnOrigin::NONE
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
# Who can answer an approval: :worker or :non_interactive (the
|
|
@@ -104,53 +105,44 @@ module Samagotchi
|
|
|
104
105
|
@protected = nil
|
|
105
106
|
end
|
|
106
107
|
|
|
107
|
-
# A turn starts
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
author =
|
|
115
|
-
|
|
116
|
-
|
|
108
|
+
# A turn starts (Engine#begin_turn, before it is published): the git
|
|
109
|
+
# facts read afresh, and a participant's turn's mode decided once for
|
|
110
|
+
# the whole turn from its origin (Engine#run_turn's TurnOrigin; nil or a
|
|
111
|
+
# Hash is read as one, TurnOrigin.of). Nothing is kept here.
|
|
112
|
+
# @param session [Session, nil] the turn's
|
|
113
|
+
# @return [TurnStart]
|
|
114
|
+
def begin_turn(origin, session:)
|
|
115
|
+
author = TurnOrigin.of(origin).participant_author
|
|
116
|
+
entry = author && @participant.call(author)
|
|
117
|
+
TurnStart.new(git: Guardrails::GitInfo.new, participant: entry,
|
|
118
|
+
mode: author && participant_turn_mode(session, entry))
|
|
117
119
|
end
|
|
118
120
|
|
|
119
121
|
# A participant's turn's mode: in a Verify fresh child the one saved
|
|
120
122
|
# on it at spawn (VerifyFreshOrigin#turn_mode: :review unless saved
|
|
121
|
-
# :pair), never looked up again; elsewhere ParticipantMode.for_turn
|
|
123
|
+
# :pair), never looked up again; elsewhere ParticipantMode.for_turn
|
|
124
|
+
# (:pair only in a working session for a participant invited to act).
|
|
122
125
|
def participant_turn_mode(session, entry)
|
|
123
126
|
fresh = session.respond_to?(:verify_fresh) && session.verify_fresh
|
|
124
127
|
fresh ? fresh.turn_mode : ParticipantMode.for_turn(session, entry)
|
|
125
128
|
end
|
|
126
129
|
private :participant_turn_mode
|
|
127
130
|
|
|
128
|
-
# The
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
# @
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# Who wrote the running turn when it isn't the session's owner (its
|
|
136
|
-
# origin's participant author); nil for the owner's turns and every
|
|
137
|
-
# other origin.
|
|
138
|
-
# @return [String, nil]
|
|
139
|
-
def turn_author = @origin.participant_author
|
|
140
|
-
|
|
141
|
-
# The gate every tool call asks first.
|
|
131
|
+
# The gate every tool call asks first, given the call's TurnContext
|
|
132
|
+
# (Guardrails::Gate#evaluate's context:): its judged context (#context),
|
|
133
|
+
# its checks (#checks) and its approval (#request_approval) are that
|
|
134
|
+
# turn's.
|
|
135
|
+
# @param hooks [Hooks::Registry, nil] the voters (the Engine's)
|
|
136
|
+
# @param tools [Tools::Registry, nil] the session's tools (the Engine's)
|
|
142
137
|
# @return [Guardrails::Gate]
|
|
143
|
-
def gate
|
|
138
|
+
def gate(hooks:, tools:)
|
|
144
139
|
Guardrails::Gate.new(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
approver: ->(verdict) { request_approval(verdict) },
|
|
140
|
+
hooks: hooks,
|
|
141
|
+
tools: tools,
|
|
142
|
+
context_for: ->(turn) { context(turn) },
|
|
143
|
+
approver: ->(verdict, turn) { request_approval(verdict, turn) },
|
|
150
144
|
approvals_lookup: -> { @approvals },
|
|
151
|
-
|
|
152
|
-
cancelled_lookup: -> { @cancelled_lookup.call },
|
|
153
|
-
tools_lookup: -> { @tools_lookup.call }
|
|
145
|
+
checks_for: ->(turn) { checks(turn) }
|
|
154
146
|
)
|
|
155
147
|
end
|
|
156
148
|
|
|
@@ -161,39 +153,42 @@ module Samagotchi
|
|
|
161
153
|
# ParticipantMode: a review turn's (ReviewBoundary) or a pair turn's
|
|
162
154
|
# (PairBoundary). Either denies what a participant's turn may not do,
|
|
163
155
|
# never asks about it; both are no-ops for the owner's turns.
|
|
164
|
-
|
|
156
|
+
# @param turn [TurnContext] the call's turn
|
|
157
|
+
def checks(turn)
|
|
165
158
|
rules = self.rules
|
|
166
|
-
[@failures, participant_boundary, rules.hook_asks, protected_paths,
|
|
167
|
-
(@scratch_writes ||= Guardrails::ScratchWrites.new if @scratch), child_boundary, rules].compact
|
|
159
|
+
[@failures, participant_boundary(turn), rules.hook_asks, protected_paths,
|
|
160
|
+
(@scratch_writes ||= Guardrails::ScratchWrites.new if @scratch), child_boundary(turn), rules].compact
|
|
168
161
|
end
|
|
169
162
|
|
|
170
|
-
# The boundary for a participant's turn in this session (#checks)
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
# The boundary for a participant's turn in this session (#checks), by
|
|
164
|
+
# the turn's mode.
|
|
165
|
+
def participant_boundary(turn)
|
|
166
|
+
turn.participant_mode == ParticipantMode::PAIR ? pair_boundary(turn) : review_boundary(turn)
|
|
173
167
|
end
|
|
174
168
|
|
|
175
169
|
# A participant's review turn's boundary (Guardrails::ReviewBoundary): a
|
|
176
|
-
# no-op unless
|
|
177
|
-
def review_boundary
|
|
178
|
-
|
|
170
|
+
# no-op unless +turn+'s author is a participant's.
|
|
171
|
+
def review_boundary(turn)
|
|
172
|
+
Guardrails::ReviewBoundary.new(author: -> { turn.author }, root: -> { boundary_root(turn.git) })
|
|
179
173
|
end
|
|
180
174
|
|
|
181
175
|
# A participant's pair turn's boundary (Guardrails::PairBoundary): a
|
|
182
|
-
# no-op unless
|
|
183
|
-
def pair_boundary
|
|
184
|
-
|
|
176
|
+
# no-op unless +turn+'s author is a participant's.
|
|
177
|
+
def pair_boundary(turn)
|
|
178
|
+
Guardrails::PairBoundary.new(author: -> { turn.author }, root: -> { boundary_root(turn.git) })
|
|
185
179
|
end
|
|
186
180
|
|
|
187
|
-
# The delegate child's boundary (Guardrails::ChildBoundary)
|
|
188
|
-
# any other session.
|
|
189
|
-
def child_boundary
|
|
181
|
+
# The delegate child's boundary (Guardrails::ChildBoundary), on +turn+'s
|
|
182
|
+
# git facts; nil for any other session.
|
|
183
|
+
def child_boundary(turn)
|
|
190
184
|
return nil unless @session_lookup.call&.delegate?
|
|
191
185
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
186
|
+
git = turn.git
|
|
187
|
+
Guardrails::ChildBoundary.new(
|
|
188
|
+
root: -> { boundary_root(git) },
|
|
189
|
+
worktrees: -> { (root = boundary_root(git)) ? git.worktrees(root) : [] },
|
|
190
|
+
common_dir: -> { (root = boundary_root(git)) && git.common_dir(root) },
|
|
191
|
+
branch: -> { (root = boundary_root(git)) && git.branch(root) }
|
|
197
192
|
)
|
|
198
193
|
end
|
|
199
194
|
|
|
@@ -285,11 +280,13 @@ module Samagotchi
|
|
|
285
280
|
end
|
|
286
281
|
end
|
|
287
282
|
|
|
288
|
-
# The context the gate sees for a tool call now
|
|
283
|
+
# The context the gate sees for a tool call of +turn+ now: the process
|
|
284
|
+
# cwd, the session, the interface, the turn's origin and git facts.
|
|
285
|
+
# @param turn [TurnContext]
|
|
289
286
|
# @return [Guardrails::Context]
|
|
290
|
-
def context
|
|
287
|
+
def context(turn)
|
|
291
288
|
Guardrails::Context.new(cwd: Dir.pwd, session_id: @session_lookup.call&.id, interface: interface,
|
|
292
|
-
origin:
|
|
289
|
+
origin: turn.origin.wire, git: turn.git)
|
|
293
290
|
end
|
|
294
291
|
|
|
295
292
|
# Ask the user to approve a call the gate voted `ask` on, through the
|
|
@@ -297,8 +294,10 @@ module Samagotchi
|
|
|
297
294
|
# verdict: allow with the picked scope, or deny with a note for the
|
|
298
295
|
# model. A --non-interactive run has no one to ask and denies at once.
|
|
299
296
|
# @param verdict [Guardrails::Verdict]
|
|
297
|
+
# @param turn [TurnContext, nil] the call's turn (its author, the
|
|
298
|
+
# participant's name); nil: the owner's
|
|
300
299
|
# @return [Guardrails::Verdict]
|
|
301
|
-
def request_approval(verdict)
|
|
300
|
+
def request_approval(verdict, turn = nil)
|
|
302
301
|
if interface == :non_interactive
|
|
303
302
|
return verdict.settle!(:deny, decided_by: "no one", note: "No one to approve it (non-interactive run).")
|
|
304
303
|
end
|
|
@@ -313,10 +312,10 @@ module Samagotchi
|
|
|
313
312
|
# verdict.call is the call that will run (a hook may have replaced it).
|
|
314
313
|
# A participant's turn's ask names them and offers once only
|
|
315
314
|
# (Approval.payload's author:), whichever session it is.
|
|
316
|
-
author =
|
|
315
|
+
author = turn&.author
|
|
317
316
|
payload = Guardrails::Approval.payload(verdict, label: label, preview: approval_preview(verdict.call),
|
|
318
317
|
only: (SCRATCH_SCOPES if @scratch), author: author,
|
|
319
|
-
author_name: (
|
|
318
|
+
author_name: (turn.participant&.name if author))
|
|
320
319
|
# A call on chi's own config, hooks or guardrails is the user's to
|
|
321
320
|
# allow, never a parent agent's (Guardrails::ParentPolicy).
|
|
322
321
|
payload[:user_only] = Guardrails::ParentApprovals.protected_facts?(payload[:approval])
|
|
@@ -324,8 +323,8 @@ module Samagotchi
|
|
|
324
323
|
# one that passes every gate is allowed once without reaching them
|
|
325
324
|
# (docs/guardrails.md, "System 1").
|
|
326
325
|
judgment = begin
|
|
327
|
-
system_one_judge(payload,
|
|
328
|
-
|
|
326
|
+
system_one_judge(payload, author: author, targets: verdict.targets,
|
|
327
|
+
ask_user: verdict.fresh_ask? || verdict.second_rule_ask?, other_rules: verdict.other_ask_rules)
|
|
329
328
|
rescue LLM::RequestCancelled
|
|
330
329
|
return turn_stopped(verdict)
|
|
331
330
|
end
|
|
@@ -370,9 +369,10 @@ module Samagotchi
|
|
|
370
369
|
# so the failures are in the log too.
|
|
371
370
|
# With +ask_user+ (a fresh ask, a second rule's) it is judged and logged, never answered.
|
|
372
371
|
# +other_rules+ are the ids of the other rules asking about the same call.
|
|
372
|
+
# +author+ is the turn's (TurnContext#author).
|
|
373
373
|
# @return [Hash, nil] {id:, p:, auto:}, nil when nothing was written
|
|
374
|
-
def system_one_judge(payload, targets: nil, ask_user: false, other_rules: nil)
|
|
375
|
-
return nil if Steer.participant_author?(
|
|
374
|
+
def system_one_judge(payload, author: nil, targets: nil, ask_user: false, other_rules: nil)
|
|
375
|
+
return nil if Steer.participant_author?(author)
|
|
376
376
|
|
|
377
377
|
client = @system_one.call
|
|
378
378
|
log = system_one_log
|
|
@@ -513,13 +513,14 @@ module Samagotchi
|
|
|
513
513
|
end
|
|
514
514
|
|
|
515
515
|
# The work tree top of the session's working_directory (the folder
|
|
516
|
-
# itself outside a repo, or when it's gone)
|
|
517
|
-
# the waker's cwd for a worker whose
|
|
518
|
-
|
|
516
|
+
# itself outside a repo, or when it's gone), asked of +git+ (the turn's
|
|
517
|
+
# GitInfo); never Dir.pwd, which is the waker's cwd for a worker whose
|
|
518
|
+
# folder is gone.
|
|
519
|
+
def boundary_root(git)
|
|
519
520
|
dir = @session_lookup.call&.working_directory.to_s
|
|
520
521
|
return nil if dir.empty?
|
|
521
522
|
|
|
522
|
-
|
|
523
|
+
git.root(dir) || File.expand_path(dir)
|
|
523
524
|
end
|
|
524
525
|
|
|
525
526
|
# The rules of a bundle that needs a newer chi: the ones last loaded
|
|
@@ -16,65 +16,65 @@ module Samagotchi
|
|
|
16
16
|
# deny. A hook may replace event[:call]; the verdict carries the final
|
|
17
17
|
# call.
|
|
18
18
|
class Gate
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# @param
|
|
26
|
-
#
|
|
27
|
-
#
|
|
19
|
+
# What a call is judged in comes with it: #evaluate's +context:+, the
|
|
20
|
+
# call's turn (TurnContext: its controller, model key and name, and
|
|
21
|
+
# what +context_for+ and +checks_for+ make of it).
|
|
22
|
+
#
|
|
23
|
+
# @param hooks [Hooks::Registry, nil] the before_tool_call voters (the
|
|
24
|
+
# Engine's registry, built before its gate; nil: no hooks)
|
|
25
|
+
# @param context_for [#call] (turn) → the Context for a call of that
|
|
26
|
+
# turn (the Engine's: session, interface, origin, the turn's git cache)
|
|
27
|
+
# @param approver [#call, nil] (verdict, turn) settles an ask
|
|
28
|
+
# (GuardrailWiring#request_approval); without one an ask is denied
|
|
28
29
|
# @param approvals_lookup [#call] returns the Approvals store (or nil)
|
|
29
|
-
# @param
|
|
30
|
-
# final call after the hooks: protected paths, then the rules
|
|
31
|
-
# @param
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@hooks_lookup = hooks_lookup
|
|
39
|
-
@context_lookup = context_lookup
|
|
40
|
-
@model_key_lookup = model_key_lookup
|
|
41
|
-
@model_name_lookup = model_name_lookup
|
|
30
|
+
# @param checks_for [#call] (turn) → core checks (#check(verdict)) run
|
|
31
|
+
# on the final call after the hooks: protected paths, then the rules
|
|
32
|
+
# @param tools [Tools::Registry, nil] the session's tools (the Engine's,
|
|
33
|
+
# built before its gate): a plugin tool's targets: say what its call
|
|
34
|
+
# acts on
|
|
35
|
+
def initialize(hooks: nil, tools: nil, context_for: ->(_turn) { Context.new }, approver: nil, approvals_lookup: -> {},
|
|
36
|
+
checks_for: ->(_turn) { [] })
|
|
37
|
+
@hooks = hooks
|
|
38
|
+
@context_for = context_for
|
|
42
39
|
@approver = approver
|
|
43
40
|
@approvals_lookup = approvals_lookup
|
|
44
|
-
@
|
|
45
|
-
@
|
|
46
|
-
@tools_lookup = tools_lookup
|
|
41
|
+
@checks_for = checks_for
|
|
42
|
+
@tools = tools
|
|
47
43
|
end
|
|
48
44
|
|
|
49
45
|
# @param call [Hash] the parsed tool call
|
|
46
|
+
# @param context [TurnContext] the call's turn: a cancelled one's
|
|
47
|
+
# controller denies the call without a vote (a hook's stop_turn: the
|
|
48
|
+
# rest of a batch); its model key and name go to the targets
|
|
49
|
+
# (memory overlays, rules' models:)
|
|
50
50
|
# @param iteration [Integer]
|
|
51
51
|
# @param params [String] the call's one-line preview
|
|
52
52
|
# @param verdict [Verdict, nil] the one the voters vote on (the test
|
|
53
53
|
# kit's recording one, maybe with a vote already in); default a new
|
|
54
54
|
# one for +call+
|
|
55
55
|
# @return [Verdict]
|
|
56
|
-
def evaluate(call, iteration:, params:, verdict: nil)
|
|
57
|
-
|
|
56
|
+
def evaluate(call, context:, iteration:, params:, verdict: nil)
|
|
57
|
+
judged = @context_for.call(context)
|
|
58
58
|
verdict ||= Verdict.new(call: call)
|
|
59
59
|
verdict.call = call
|
|
60
|
-
if
|
|
60
|
+
if context.cancelled?
|
|
61
61
|
verdict.deny!("the turn was stopped", decided_by: "core")
|
|
62
|
-
verdict.context =
|
|
63
|
-
verdict.targets = targets_for(call, context)
|
|
62
|
+
verdict.context = judged
|
|
63
|
+
verdict.targets = targets_for(call, judged, context)
|
|
64
64
|
return verdict
|
|
65
65
|
end
|
|
66
66
|
before = { type: :before_tool_call, iteration: iteration, call: call.dup, params: params,
|
|
67
67
|
blocked: verdict.deny?, block_reason: (verdict.reason if verdict.deny?), guardrail: verdict,
|
|
68
|
-
context:
|
|
68
|
+
context: judged.to_h, targets: targets_for(call, judged, context).to_h }
|
|
69
69
|
fire_each(:before_tool_call, before) do |event|
|
|
70
70
|
verdict.legacy_deny!(event[:block_reason]) if event[:blocked]
|
|
71
71
|
event[:blocked] = verdict.deny?
|
|
72
72
|
event[:block_reason] = verdict.reason if verdict.deny?
|
|
73
73
|
end
|
|
74
74
|
verdict.call = before[:call] || call
|
|
75
|
-
verdict.context =
|
|
76
|
-
verdict.targets = targets_for(verdict.call, context)
|
|
77
|
-
Array(@
|
|
75
|
+
verdict.context = judged
|
|
76
|
+
verdict.targets = targets_for(verdict.call, judged, context)
|
|
77
|
+
Array(@checks_for.call(context)).each { |check| check.check(verdict) }
|
|
78
78
|
verdict
|
|
79
79
|
end
|
|
80
80
|
|
|
@@ -83,7 +83,8 @@ module Samagotchi
|
|
|
83
83
|
# @return [Verdict]
|
|
84
84
|
# A stored approval that covers the call allows it without asking; an
|
|
85
85
|
# answer that allows it beyond this once is stored.
|
|
86
|
-
|
|
86
|
+
# @param context [TurnContext] the call's turn (the approver's)
|
|
87
|
+
def settle_ask(verdict, context:)
|
|
87
88
|
approvals = @approvals_lookup.call
|
|
88
89
|
if !verdict.fresh_ask? && (entry = stored(approvals, verdict))
|
|
89
90
|
verdict.settle!(:allow, decided_by: "approval")
|
|
@@ -92,7 +93,7 @@ module Samagotchi
|
|
|
92
93
|
end
|
|
93
94
|
return verdict.settle!(:deny, decided_by: "no one", note: "No one to approve it.") unless @approver
|
|
94
95
|
|
|
95
|
-
@approver.call(verdict)
|
|
96
|
+
@approver.call(verdict, context)
|
|
96
97
|
return verdict.settle!(:deny, decided_by: "no one", note: "No one to approve it.") if verdict.ask?
|
|
97
98
|
|
|
98
99
|
remember(approvals, verdict)
|
|
@@ -118,14 +119,14 @@ module Samagotchi
|
|
|
118
119
|
Log.warn(:guardrails, "approval_store_failed", echo: "[samagotchi:guardrails] could not store the approval: #{e.class}: #{e.message}", error: e.class.name)
|
|
119
120
|
end
|
|
120
121
|
|
|
121
|
-
def targets_for(call,
|
|
122
|
-
Targets.for(call,
|
|
123
|
-
|
|
122
|
+
def targets_for(call, judged, turn)
|
|
123
|
+
Targets.for(call, judged, model_key: turn.model_key, model_name: turn.model_name, registry: @tools,
|
|
124
|
+
surface: turn.surface)
|
|
124
125
|
end
|
|
125
126
|
|
|
126
127
|
# Registry#fire_each rescues a raising hook itself.
|
|
127
128
|
def fire_each(name, event, &)
|
|
128
|
-
@
|
|
129
|
+
@hooks&.fire_each(name, event, &)
|
|
129
130
|
end
|
|
130
131
|
end
|
|
131
132
|
end
|
|
@@ -146,7 +146,8 @@ module Samagotchi
|
|
|
146
146
|
)
|
|
147
147
|
|
|
148
148
|
# @param author [#call] → String, nil: the running turn's author
|
|
149
|
-
# (
|
|
149
|
+
# (the call's TurnContext#author, GuardrailWiring#review_boundary /
|
|
150
|
+
# #pair_boundary); the check is off unless it is a
|
|
150
151
|
# participant's (Steer.participant_author?)
|
|
151
152
|
# @param denied_dirs [#call] → Array<String>: chi's own dirs
|
|
152
153
|
# @param home [#call] → String, nil
|
|
@@ -45,7 +45,8 @@ module Samagotchi
|
|
|
45
45
|
MEMORY_SCOPE = "a participant's review turn reads project memories only: call memory_read with scope: project"
|
|
46
46
|
|
|
47
47
|
# @param author [#call] → String, nil: the running turn's author
|
|
48
|
-
# (
|
|
48
|
+
# (the call's TurnContext#author, GuardrailWiring#review_boundary /
|
|
49
|
+
# #pair_boundary); the check is off unless it is a
|
|
49
50
|
# participant's (Steer.participant_author?)
|
|
50
51
|
# @param root [#call] → String, nil: the session's project root (the
|
|
51
52
|
# work tree top of its folder); nil denies every read
|
|
@@ -113,7 +113,9 @@ module Samagotchi
|
|
|
113
113
|
# @param model_name [String, nil] the bare model name, for a rule's models:
|
|
114
114
|
# @param registry [Tools::Registry, nil] the session's tools: a plugin
|
|
115
115
|
# tool's entry says what it acts on (its targets:)
|
|
116
|
-
|
|
116
|
+
# @param surface [Tools::ToolSurface, nil] the turn's: call_tool's
|
|
117
|
+
# targets name what the turn reaches (nil: the Registry now)
|
|
118
|
+
def self.for(call, context, model_key: nil, model_name: nil, registry: nil, surface: nil)
|
|
117
119
|
tool = call[:name].to_s
|
|
118
120
|
base = context.cwd
|
|
119
121
|
command = nil
|
|
@@ -136,7 +138,7 @@ module Samagotchi
|
|
|
136
138
|
when "memory_write"
|
|
137
139
|
paths << memory_path(call, model_key)
|
|
138
140
|
else
|
|
139
|
-
given = plugin_targets(call, registry)
|
|
141
|
+
given = plugin_targets(call, registry, surface)
|
|
140
142
|
command = given[:command]
|
|
141
143
|
given_cwd = given[:cwd].to_s.strip
|
|
142
144
|
cwd = File.expand_path(given_cwd, base) unless given_cwd.empty?
|
|
@@ -163,12 +165,12 @@ module Samagotchi
|
|
|
163
165
|
# logged) when it names a core tool or another bundle's tool, so a
|
|
164
166
|
# call can't pass for one and match (or reuse the approvals of) its
|
|
165
167
|
# rules. label: counts when it is one short line (.label_of).
|
|
166
|
-
def self.plugin_targets(call, registry)
|
|
168
|
+
def self.plugin_targets(call, registry, surface = nil)
|
|
167
169
|
entry = registry && registry[call[:name].to_s]
|
|
168
170
|
none = { command: nil, paths: [], cwd: nil, acts_as: nil, args: nil, label: nil }
|
|
169
171
|
return none unless entry && !entry.core? && entry.targets
|
|
170
172
|
|
|
171
|
-
given = entry.
|
|
173
|
+
given = entry.targets_of(call, surface)
|
|
172
174
|
return none unless given.is_a?(Hash)
|
|
173
175
|
|
|
174
176
|
given = given.transform_keys(&:to_sym)
|
|
@@ -166,7 +166,7 @@ module Samagotchi
|
|
|
166
166
|
# The rule set as a worker would load it now, the rule's id looked up in
|
|
167
167
|
# it. Every other answer than ASKS names what stands in the way.
|
|
168
168
|
def rule_status(id)
|
|
169
|
-
wiring = GuardrailWiring.new(scratch: false,
|
|
169
|
+
wiring = GuardrailWiring.new(scratch: false, tools: -> {}, session: -> {},
|
|
170
170
|
cancelled: -> { false }, ask: ->(_fields) {})
|
|
171
171
|
rules = wiring.rules
|
|
172
172
|
failed = wiring.failures.required
|
|
@@ -473,7 +473,9 @@ module Samagotchi
|
|
|
473
473
|
parsed = safe_parse(@engine.messages_json_for_recap)
|
|
474
474
|
return :too_short if parsed.nil?
|
|
475
475
|
|
|
476
|
-
|
|
476
|
+
# The turns as the page counts them (Steer.turn_opening?): not Qwen's
|
|
477
|
+
# recovery line, nor input merged into a running turn.
|
|
478
|
+
user_turns = parsed.count { |message| Steer.turn_opening?(message) }
|
|
477
479
|
if parsed.empty? || user_turns < @min_user_turns
|
|
478
480
|
drop_stale(parsed)
|
|
479
481
|
return :too_short
|