samagotchi 0.154.1 → 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 +16 -1
- data/docs/architecture.md +3 -2
- data/docs/bundles.md +21 -0
- data/docs/cli.md +1 -1
- 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/engine.md +220 -99
- 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 +9 -5
- data/docs/internals/web-frontend.md +1 -1
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +2 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/system/manifest.yml +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/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/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +190 -213
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +63 -41
- 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/system_prompt.rb +37 -30
- 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 +91 -100
- 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_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/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +1 -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/composer_fold.js +3 -0
- 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/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
- metadata +17 -1
|
@@ -4,9 +4,9 @@ module Samagotchi
|
|
|
4
4
|
class Engine
|
|
5
5
|
# The session's toolkit: the kits and what a turn declares
|
|
6
6
|
# (ToolkitBinding, forwarded), ctx.load_tools and the staged loads a turn
|
|
7
|
-
# appends, and System 1's toolkit suggestion (
|
|
8
|
-
#
|
|
9
|
-
# @
|
|
7
|
+
# appends, and System 1's toolkit suggestion (TurnStages::ToolkitSuggestion,
|
|
8
|
+
# forwarded). Reads @toolkit_binding, @tools, @tool_index, @tool_loads,
|
|
9
|
+
# @toolkit_suggestion, @session.
|
|
10
10
|
module ToolkitPart
|
|
11
11
|
private
|
|
12
12
|
|
|
@@ -17,22 +17,29 @@ module Samagotchi
|
|
|
17
17
|
def toolkit_explained(toolkit = nil) = @toolkit_binding.explained(toolkit)
|
|
18
18
|
def toolkit_summary = @toolkit_binding.summary
|
|
19
19
|
|
|
20
|
+
# The Registry published for readers off the turn thread
|
|
21
|
+
# (ToolkitBinding#publish!): at the end of the Engine's build, after a
|
|
22
|
+
# turn's staged tool sets and its toolkit apply.
|
|
23
|
+
def publish_tools! = @toolkit_binding.publish!
|
|
24
|
+
|
|
20
25
|
# The tools this session registered (Tools::Registry), declared or not.
|
|
21
26
|
attr_reader :tools
|
|
22
27
|
|
|
23
28
|
public :tools, :toolkit_kits, :resolved_toolkit, :toolkit_explained, :toolkit_summary
|
|
24
29
|
|
|
25
30
|
# A plugin's ctx.load_tools: stage the reachable tools +names+ names
|
|
26
|
-
# (Tools::ToolIndex#deferred_name
|
|
27
|
-
# nor loaded with an earlier tool change, for
|
|
28
|
-
# that names no tool at all is logged; a
|
|
29
|
-
# skipped quietly.
|
|
31
|
+
# (Tools::ToolIndex#deferred_name, on the published Registry) that the
|
|
32
|
+
# session neither declares nor loaded with an earlier tool change, for
|
|
33
|
+
# bundle +bundle+. A name that names no tool at all is logged; a
|
|
34
|
+
# declared or removed one is skipped quietly.
|
|
30
35
|
# @return [Array<String>] the wire names staged
|
|
31
36
|
def load_tools(names, bundle)
|
|
32
37
|
added = ToolLoads.added(@session&.messages)
|
|
38
|
+
published = @toolkit_binding.published
|
|
39
|
+
surface = Tools::ToolSurface.for(published)
|
|
33
40
|
wanted = names.filter_map do |name|
|
|
34
|
-
wire = @tool_index.deferred_name(name)
|
|
35
|
-
Log.info(:plugins, "load_tools_skipped", bundle: bundle, tool: name) unless wire ||
|
|
41
|
+
wire = @tool_index.deferred_name(name, surface)
|
|
42
|
+
Log.info(:plugins, "load_tools_skipped", bundle: bundle, tool: name) unless wire || published.key?(name)
|
|
36
43
|
wire
|
|
37
44
|
end
|
|
38
45
|
@tool_loads.stage(wanted - added, by: bundle)
|
|
@@ -79,191 +86,27 @@ module Samagotchi
|
|
|
79
86
|
def apply_toolkit!(session, on_event = nil) = @toolkit_binding.apply!(session, on_event)
|
|
80
87
|
private :apply_toolkit!
|
|
81
88
|
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# returns the ask's Pending, which the turn joins before apply_toolkit!
|
|
87
|
-
# (#settle_toolkit_suggestion), with the turn's controller as the
|
|
88
|
-
# request's cancel controller.
|
|
89
|
-
# @return [ToolkitSuggest::Pending, nil] nil unless the turn joins an ask
|
|
90
|
-
def start_toolkit_suggestion(turn)
|
|
91
|
-
mode = Config.get("toolkit.suggest.mode").to_s
|
|
92
|
-
return if mode == "off"
|
|
93
|
-
|
|
94
|
-
joined = mode == "on"
|
|
95
|
-
session = turn.session
|
|
96
|
-
default = Toolkit.of(session.toolkit)
|
|
97
|
-
log = toolkit_suggestion_log
|
|
98
|
-
# The predicate checks the session first; the kits (memory kits are
|
|
99
|
-
# read from disk) and the log (read whole) only when it gets there.
|
|
100
|
-
kits = nil
|
|
101
|
-
candidates = nil
|
|
102
|
-
return unless ToolkitSuggest.eligible?(
|
|
103
|
-
session: session, prompt: turn.prompt, author: turn.origin.author,
|
|
104
|
-
candidate_count: lambda {
|
|
105
|
-
kits = @toolkit_binding.kits
|
|
106
|
-
candidates = ToolkitSuggest.candidates(kits, default_name: default.name)
|
|
107
|
-
candidates.size
|
|
108
|
-
},
|
|
109
|
-
last_suggestion: -> { ToolkitSuggest.last_suggestion(log, session.id) },
|
|
110
|
-
defer_turns: Config.get("toolkit.suggest.defer_turns"),
|
|
111
|
-
defer_tokens: Config.get("toolkit.suggest.defer_tokens")
|
|
112
|
-
)
|
|
89
|
+
# System 1's toolkit suggestion (TurnStages::ToolkitSuggestion):
|
|
90
|
+
# prepare_turn starts, settles and abandons it; the Worker warms it.
|
|
91
|
+
attr_reader :toolkit_suggestion
|
|
92
|
+
private :toolkit_suggestion
|
|
113
93
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
Log.info(:toolkit, "toolkit_suggest_in_flight_skipped", session_id: session.id)
|
|
118
|
-
return
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
client = system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :toolkit),
|
|
122
|
-
ToolkitSuggest::JUDGMENT, session, mode: Config.get("toolkit.suggest.mode").to_s)
|
|
123
|
-
return unless client
|
|
124
|
-
|
|
125
|
-
# The warm-up's only value is the cache: the real ask never queues
|
|
126
|
-
# behind it.
|
|
127
|
-
stop_toolkit_warmup
|
|
128
|
-
|
|
129
|
-
# Rules, not tool lists: this runs before await_init_tasks and
|
|
130
|
-
# apply_staged_tools!, so a first turn's plugin/MCP tools aren't
|
|
131
|
-
# registered yet; the report expands the rules (ToolkitSuggest.rules).
|
|
132
|
-
snapshots = candidates.map do |kit|
|
|
133
|
-
ToolkitSuggest::Candidate.new(name: kit.name.dup.freeze, description: kit.description.dup.freeze,
|
|
134
|
-
rules: ToolkitSuggest.rules(Toolkit.resolve_kit(kit.name, kits)))
|
|
135
|
-
end.freeze
|
|
136
|
-
default_snapshot = ToolkitSuggest::Candidate.new(name: default.name.dup.freeze, description: nil,
|
|
137
|
-
rules: ToolkitSuggest.rules(default.resolve(kits)))
|
|
138
|
-
registered = @tools.entries(toolkit: nil).reject { |entry| entry.source == Tools::ToolIndex::SOURCE }
|
|
139
|
-
core_tools, plugin_tools = registered.partition(&:core?).map { |entries| entries.map { |entry| entry.name.dup.freeze }.freeze }
|
|
140
|
-
state = ToolkitSuggest.question(
|
|
141
|
-
default: default, kits: kits, project: File.basename(session.project_root.to_s),
|
|
142
|
-
prompt: turn.prompt
|
|
143
|
-
).last
|
|
144
|
-
snapshot = ToolkitSuggest::Snapshot.new(
|
|
145
|
-
session_id: session.id.dup.freeze, turn_number: ToolkitSuggest.turn_number(session.messages),
|
|
146
|
-
default: default_snapshot, candidates: snapshots, core_tools: core_tools, plugin_tools: plugin_tools,
|
|
147
|
-
state: { project: state[:project].dup.freeze, prompt: state[:prompt].dup.freeze }.freeze
|
|
148
|
-
)
|
|
149
|
-
threshold = Config.get("toolkit.suggest.threshold")
|
|
150
|
-
runs = session.scratch ? nil : system_one_runs
|
|
151
|
-
pending = ToolkitSuggest::Pending.new(log: log, joined: joined,
|
|
152
|
-
acted: runs && ->(judged, did) { runs.acted(judged.fields[:run_id], did) },
|
|
153
|
-
timeout_seconds: SystemOne::Client.configured_timeout_ms / 1000.0)
|
|
154
|
-
# On mode: cut by a Stop while the turn waits (Pending#wait), not
|
|
155
|
-
# by one after it went on; audit's ask is never cut.
|
|
156
|
-
controller = joined ? pending.cancel_controller : nil
|
|
157
|
-
@side_judgments.start(:toolkit, warn: ToolkitSuggest.method(:warn), finish: pending.method(:finish)) do
|
|
158
|
-
ToolkitSuggest.judge(client: client, snapshot: snapshot, threshold: threshold,
|
|
159
|
-
cancel_controller: controller)
|
|
160
|
-
end
|
|
161
|
-
joined ? pending : nil
|
|
162
|
-
rescue StandardError => e
|
|
163
|
-
ToolkitSuggest.warn(e)
|
|
164
|
-
nil
|
|
165
|
-
end
|
|
166
|
-
private :start_toolkit_suggestion
|
|
167
|
-
|
|
168
|
-
# On mode, before apply_toolkit!: wait for the ask (#start_toolkit_
|
|
169
|
-
# suggestion) up to System 1's timeout from its start, or until a Stop,
|
|
170
|
-
# and put the session on a candidate picked with confidence ≥ threshold
|
|
171
|
-
# (ToolkitBinding#suggest: origin suggested, saved, the notice). Any
|
|
172
|
-
# other outcome (the default, below threshold, undecided, no judgment,
|
|
173
|
-
# a timeout, a Stop) leaves the session on its default. Nothing here
|
|
174
|
-
# sends a request; prepare_turn's cancel check ends a stopped turn. A
|
|
175
|
-
# Stop wins over an answer already in (Pending#wait checks it first).
|
|
176
|
-
def settle_toolkit_suggestion(turn, pending)
|
|
177
|
-
return if pending.nil?
|
|
178
|
-
|
|
179
|
-
outcome, judged = pending.wait(turn.controller)
|
|
180
|
-
if outcome == :timeout
|
|
181
|
-
# The session starts on its default without a word otherwise: a
|
|
182
|
-
# slow System 1 host (a local one) is the usual cause.
|
|
183
|
-
timeout_ms = SystemOne::Client.configured_timeout_ms
|
|
184
|
-
Log.warn(:toolkit, "toolkit_suggest_timeout", session_id: turn.session.id, timeout_ms: timeout_ms,
|
|
185
|
-
reason: "System 1 gave no answer within system_one.timeout_ms")
|
|
186
|
-
@toolkit_binding.fell_back(turn.session, "System 1 took over #{timeout_ms} ms")
|
|
187
|
-
return
|
|
188
|
-
end
|
|
189
|
-
unless outcome == :answered
|
|
190
|
-
Log.info(:toolkit, "toolkit_suggest_#{outcome}", session_id: turn.session.id)
|
|
191
|
-
return
|
|
192
|
-
end
|
|
193
|
-
return if judged.nil?
|
|
194
|
-
|
|
195
|
-
pending.record(toolkit_suggestion_outcome(turn.session, judged))
|
|
196
|
-
rescue StandardError => e
|
|
197
|
-
ToolkitSuggest.warn(e)
|
|
198
|
-
pending&.record("failed")
|
|
199
|
-
end
|
|
200
|
-
private :settle_toolkit_suggestion
|
|
201
|
-
|
|
202
|
-
# What the turn does with an answered ask, as the log line's did:
|
|
203
|
-
# suggested (switched), chosen (the toolkit isn't the default any
|
|
204
|
-
# more), unavailable (the kit is gone), defer or default.
|
|
205
|
-
def toolkit_suggestion_outcome(session, judged)
|
|
206
|
-
case judged.would
|
|
207
|
-
when "suggested"
|
|
208
|
-
return "chosen" unless session.toolkit_origin == "default"
|
|
209
|
-
return "unavailable" unless @toolkit_binding.kits.key?(judged.suggestion.kit)
|
|
210
|
-
|
|
211
|
-
switched = @toolkit_binding.suggest(session, judged.suggestion.kit, confidence: judged.suggestion.confidence,
|
|
212
|
-
deferred: ToolkitSuggest.model_reply?(session.messages))
|
|
213
|
-
switched ? "suggested" : "chosen"
|
|
214
|
-
when "defer" then "defer"
|
|
215
|
-
when "no_judgment"
|
|
216
|
-
# System 1 failed (its own warn) or answered nothing usable.
|
|
217
|
-
@toolkit_binding.fell_back(session, "System 1 gave no answer")
|
|
218
|
-
"default"
|
|
219
|
-
else "default"
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
private :toolkit_suggestion_outcome
|
|
223
|
-
|
|
224
|
-
# On mode, as an idle worker starts (Worker#start_background): one ask
|
|
225
|
-
# with the real question and a stand-in prompt, on its own thread,
|
|
226
|
-
# never logged and never waited for, so System 1's server has the
|
|
227
|
-
# question's prefix cached for the first prompt. Only for a session a
|
|
228
|
-
# first prompt could be asked for: origin default, no model reply, not
|
|
229
|
-
# a delegate child or a review session, at least two candidates; and
|
|
230
|
-
# not when the worker starts on a first prompt (that ask comes at once).
|
|
231
|
-
def warm_toolkit_suggestion!
|
|
232
|
-
return unless Config.get("toolkit.suggest.mode").to_s == "on"
|
|
233
|
-
|
|
234
|
-
session = @session
|
|
235
|
-
return if session.nil? || session.toolkit_origin != "default" || session.delegate? || session.review?
|
|
236
|
-
return if ToolkitSuggest.model_reply?(session.messages) || !session.last_prompt.to_s.strip.empty?
|
|
237
|
-
|
|
238
|
-
default = Toolkit.of(session.toolkit)
|
|
239
|
-
kits = @toolkit_binding.kits
|
|
240
|
-
return if ToolkitSuggest.candidates(kits, default_name: default.name).size < 2
|
|
241
|
-
|
|
242
|
-
client = SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :toolkit)
|
|
243
|
-
return unless client
|
|
244
|
-
|
|
245
|
-
project = File.basename(session.project_root.to_s)
|
|
246
|
-
@toolkit_warmup_thread = Thread.new do
|
|
247
|
-
ToolkitSuggest.warm(client: client, default: default, kits: kits, project: project)
|
|
248
|
-
end
|
|
249
|
-
rescue StandardError => e
|
|
250
|
-
ToolkitSuggest.warn(e)
|
|
251
|
-
nil
|
|
252
|
-
end
|
|
94
|
+
# On mode, as an idle worker starts (Worker#start_background): the
|
|
95
|
+
# warm-up ask (TurnStages::ToolkitSuggestion#warm!).
|
|
96
|
+
def warm_toolkit_suggestion! = @toolkit_suggestion.warm!(@session)
|
|
253
97
|
public :warm_toolkit_suggestion!
|
|
254
98
|
|
|
255
|
-
#
|
|
256
|
-
#
|
|
257
|
-
def
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
SideJudgments.log(ToolkitSuggest.log_path(session_state_dir), tag: :toolkit, event: "toolkit_suggest_log_failed")
|
|
99
|
+
# The stage over this Engine's side asks, binding and tools (built in
|
|
100
|
+
# #build_tools_and_plugins, before the plugins load).
|
|
101
|
+
def build_toolkit_suggestion
|
|
102
|
+
TurnStages::ToolkitSuggestion.new(
|
|
103
|
+
side_judgments: @side_judgments, host_registry: @host_registry, toolkit_binding: @toolkit_binding,
|
|
104
|
+
tools: @tools, state_dir: -> { session_state_dir },
|
|
105
|
+
recording: ->(client, judgment, session, mode:) { system_one_recording(client, judgment, session, mode: mode) },
|
|
106
|
+
runs: -> { system_one_runs }
|
|
107
|
+
)
|
|
265
108
|
end
|
|
266
|
-
private :
|
|
109
|
+
private :build_toolkit_suggestion
|
|
267
110
|
end
|
|
268
111
|
end
|
|
269
112
|
end
|
|
@@ -117,7 +117,7 @@ module Samagotchi
|
|
|
117
117
|
# a display version of the answer (AnswerDisplay) and event[:refs]
|
|
118
118
|
# for refs on it (AnswerRefs).
|
|
119
119
|
session = turn.session
|
|
120
|
-
|
|
120
|
+
@objectives_stage.outgoing(turn, result) unless canceled || result.exhausted?
|
|
121
121
|
display = AnswerDisplay.new(session.messages)
|
|
122
122
|
refs = AnswerRefs.new(session.messages, cwd: session.working_directory)
|
|
123
123
|
after_turn = { type: :after_turn, status: canceled ? "canceled" : "completed",
|
|
@@ -5,8 +5,9 @@ module Samagotchi
|
|
|
5
5
|
# What a turn's steps use before and around the model: its settings,
|
|
6
6
|
# images and vision, the model ref and resumed run start it generates
|
|
7
7
|
# with, the used memories it publishes, and the turn-end warm-up of the
|
|
8
|
-
# next one. Writes @next_turn_waiting;
|
|
9
|
-
# @prompt_warmup, @thinking_notices,
|
|
8
|
+
# next one (TurnStages::Warmup, built here). Writes @next_turn_waiting;
|
|
9
|
+
# reads @model_settings, @kernel, @prompt_warmup, @thinking_notices,
|
|
10
|
+
# @turn_state, @turn_warmup.
|
|
10
11
|
module TurnHelpersPart
|
|
11
12
|
# The turn's settings for the kernel (LLM::TurnSettings): vision,
|
|
12
13
|
# sampling and thinking, the thinking resolved before the hooks run.
|
|
@@ -34,44 +35,20 @@ module Samagotchi
|
|
|
34
35
|
# warm-up then. nil: neither.
|
|
35
36
|
attr_accessor :next_turn_waiting
|
|
36
37
|
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return if reminders_due? || @turn_state.carried_steers? || @next_turn_waiting&.call
|
|
50
|
-
|
|
51
|
-
messages = ContextNote.with_system_head(turn.session.messages.dup, { role: "system", content: system_prompt(model) })
|
|
52
|
-
# Under the next turn's strategy (a /model switch may have changed it);
|
|
53
|
-
# the last turn's when it can't be resolved.
|
|
54
|
-
prompt, images = @kernel.warmup_prompt(messages, llm_context: @model_settings.turn_llm_context || @kernel.turn_settings&.llm_context)
|
|
55
|
-
return unless prompt
|
|
56
|
-
|
|
57
|
-
started = @prompt_warmup.start(client: client, prompt: prompt, model: @kernel.turn_settings.model_name,
|
|
58
|
-
slot: @kernel.last_slot, images: images)
|
|
59
|
-
@kernel.warmed_up! if started
|
|
60
|
-
rescue StandardError => e
|
|
61
|
-
Log.warn(:model, "warmup_skipped", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
62
|
-
end
|
|
63
|
-
private :warm_up_next_turn
|
|
64
|
-
|
|
65
|
-
# Never a paid or remote host (cache.warmup auto means local only), nor
|
|
66
|
-
# the chat loop (Splash keeps its own cache; llama.cpp's chat path is
|
|
67
|
-
# not measured), nor a client that can't warm up (a spec's stub).
|
|
68
|
-
def warmup_target?(model, client)
|
|
69
|
-
return false if model.chat? || model.entry.remote?
|
|
70
|
-
return false unless client.respond_to?(:warm_up)
|
|
71
|
-
|
|
72
|
-
PromptWarmup.enabled?
|
|
38
|
+
# The turn-end warm-up's stage (TurnStages::Warmup).
|
|
39
|
+
attr_reader :turn_warmup
|
|
40
|
+
private :turn_warmup
|
|
41
|
+
|
|
42
|
+
# The stage over the kernel and the Engine's PromptWarmup (built in
|
|
43
|
+
# #initialize, once the kernel is).
|
|
44
|
+
def build_turn_warmup
|
|
45
|
+
TurnStages::Warmup.new(
|
|
46
|
+
prompt_warmup: @prompt_warmup, kernel: @kernel, turn_state: @turn_state, model_settings: @model_settings,
|
|
47
|
+
model: -> { effective_model }, system_prompt: ->(model) { system_prompt(model) },
|
|
48
|
+
reminders_due: -> { reminders_due? }, next_turn_waiting: -> { next_turn_waiting&.call }
|
|
49
|
+
)
|
|
73
50
|
end
|
|
74
|
-
private :
|
|
51
|
+
private :build_turn_warmup
|
|
75
52
|
|
|
76
53
|
# The ref (host:model, as asked) a turn's generations ran on: the model
|
|
77
54
|
# #generate resolved, after the before_turn hooks (which may switch it).
|
|
@@ -2,152 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Samagotchi
|
|
4
4
|
class Engine
|
|
5
|
-
# Verify and Revise anchors
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# and the sessions it loads through Engine methods.
|
|
5
|
+
# Verify and Revise anchors (TurnStages::VerifyAnchors): #begin_turn
|
|
6
|
+
# checks a Verify turn's claim, #end_turn's event step stamps the opening
|
|
7
|
+
# and records the verdict. Reads @verify_anchors.
|
|
9
8
|
module VerifyPart
|
|
10
|
-
|
|
11
|
-
# wake turn's note) in the messages it keeps: display metadata, never
|
|
12
|
-
# in the prompt (LLM::ChatLoop#plain keeps them, no wire sends them).
|
|
13
|
-
# A Verify turn's claim however it ended; a Revise turn's draft only
|
|
14
|
-
# once it completed with an answer (#revision_target). Returns the
|
|
15
|
-
# stamped ReviseAnchor (the end event's revises), or nil.
|
|
16
|
-
def stamp_opening(messages, turn, outcome)
|
|
17
|
-
return unless messages.is_a?(Array)
|
|
9
|
+
private
|
|
18
10
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return if fields.empty?
|
|
11
|
+
# The anchors' stage (TurnStages::VerifyAnchors).
|
|
12
|
+
attr_reader :verify_anchors
|
|
22
13
|
|
|
23
|
-
|
|
24
|
-
Steer.turn_prompt?(message) && (message[:turn_id] || message["turn_id"]).to_s == turn.id
|
|
25
|
-
end
|
|
26
|
-
return unless opening
|
|
27
|
-
|
|
28
|
-
opening.merge!(fields)
|
|
29
|
-
revises
|
|
30
|
-
end
|
|
31
|
-
private :stamp_opening
|
|
32
|
-
|
|
33
|
-
# The turn a Revise turn revised (D6: "this turn revised that one"):
|
|
34
|
-
# only a completed turn with an answer (AnswerTail) has revised
|
|
35
|
-
# anything. A draft no prompt in the messages carries is no target (a
|
|
36
|
-
# stale page, a rolled-back turn); one a stamped turn already revised
|
|
37
|
-
# is re-targeted to its stack's top, the newest version, which the
|
|
38
|
-
# person meant ("this draft").
|
|
39
|
-
def revision_target(messages, turn, outcome)
|
|
40
|
-
return unless turn.revises && outcome == "completed"
|
|
41
|
-
|
|
42
|
-
slice = AnswerTail.turn_slice(messages, turn.id)
|
|
43
|
-
return unless slice&.any? { |message| AnswerTail.answer?(message) }
|
|
44
|
-
|
|
45
|
-
target = turn.revises.turn_id
|
|
46
|
-
seen = [turn.id]
|
|
47
|
-
until seen.include?(target)
|
|
48
|
-
return reject_revise_anchor(turn.revises, :turn_missing) unless AnswerTail.turn_slice(messages, target)
|
|
49
|
-
|
|
50
|
-
seen << target
|
|
51
|
-
newer = messages.reverse.find do |message|
|
|
52
|
-
Steer.turn_prompt?(message) && ReviseAnchor.from(message[:revises] || message["revises"])&.turn_id == target
|
|
53
|
-
end
|
|
54
|
-
return ReviseAnchor.new(turn_id: target) unless newer
|
|
55
|
-
|
|
56
|
-
target = (newer[:turn_id] || newer["turn_id"]).to_s
|
|
57
|
-
end
|
|
58
|
-
reject_revise_anchor(turn.revises, :cycle)
|
|
59
|
-
end
|
|
60
|
-
private :revision_target
|
|
61
|
-
|
|
62
|
-
def reject_revise_anchor(anchor, reason)
|
|
63
|
-
Log.warn(:turn, "revise_anchor_rejected", turn_id: anchor.turn_id, reason:)
|
|
64
|
-
nil
|
|
65
|
-
end
|
|
66
|
-
private :reject_revise_anchor
|
|
67
|
-
|
|
68
|
-
def verified_anchor(session, anchor)
|
|
69
|
-
return unless anchor
|
|
70
|
-
|
|
71
|
-
source = verify_anchor_source_session(session, anchor)
|
|
72
|
-
return unless source
|
|
73
|
-
|
|
74
|
-
turn = AnswerTail.turn_slice(Array(source.messages), anchor.turn_id)
|
|
75
|
-
return reject_verify_anchor(anchor, :turn_missing) unless turn
|
|
76
|
-
|
|
77
|
-
reply = turn.reverse.find { |message| AnswerTail.answer?(message) }
|
|
78
|
-
return reject_verify_anchor(anchor, :reply_missing) unless reply
|
|
79
|
-
|
|
80
|
-
quote_text = verify_comparison_text(anchor.quote)
|
|
81
|
-
quoted = !quote_text.empty? && verify_quote_in?(quote_text, (reply[:content] || reply["content"]).to_s, source)
|
|
82
|
-
return reject_verify_anchor(anchor, :quote_missing) unless quoted
|
|
83
|
-
|
|
84
|
-
anchor
|
|
85
|
-
end
|
|
86
|
-
private :verified_anchor
|
|
87
|
-
|
|
88
|
-
# Whether +quote_text+ is in +answer+ as the model wrote it, or as the
|
|
89
|
-
# pages show it: a page quotes its own text, which reads the session's
|
|
90
|
-
# folder relative, the home folder as ~ and a secret shape as
|
|
91
|
-
# [redacted] (a participant's page gets it so, Redact;
|
|
92
|
-
# the owner's page rewrites its quote the same way, claim_link.js
|
|
93
|
-
# normalizeClaim, so claim links match both pages). +source+ is the
|
|
94
|
-
# session whose folder that is.
|
|
95
|
-
def verify_quote_in?(quote_text, answer, source)
|
|
96
|
-
return true if verify_comparison_text(answer).include?(quote_text)
|
|
97
|
-
|
|
98
|
-
viewer = Redact::Viewer.new(cwd: source.working_directory)
|
|
99
|
-
verify_comparison_text(Redact.text(answer, viewer)).include?(quote_text)
|
|
100
|
-
end
|
|
101
|
-
private :verify_quote_in?
|
|
102
|
-
|
|
103
|
-
def verify_anchor_source_session(session, anchor)
|
|
104
|
-
return session unless session.verify_fresh
|
|
105
|
-
|
|
106
|
-
fresh = session.verify_fresh
|
|
107
|
-
return reject_verify_anchor(anchor, :fresh_anchor_mismatch) unless fresh.anchor == anchor
|
|
108
|
-
return reject_verify_anchor(anchor, :fresh_parent_missing) unless session.parent_id
|
|
109
|
-
|
|
110
|
-
Session.load(session.parent_id, state_dir: session_state_dir)
|
|
111
|
-
rescue ArgumentError, SystemCallError, IOError => e
|
|
112
|
-
Log.warn(:turn, "verify_anchor_rejected", turn_id: anchor.turn_id, reason: :source_session_unavailable,
|
|
113
|
-
error: e.class.name)
|
|
114
|
-
nil
|
|
115
|
-
end
|
|
116
|
-
private :verify_anchor_source_session
|
|
117
|
-
|
|
118
|
-
def verify_comparison_text(text)
|
|
119
|
-
OutputFormatter.strip_markup(text)
|
|
120
|
-
.gsub(/!\[([^\]]*)\]\([^)]+\)/, '\1')
|
|
121
|
-
.gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
|
|
122
|
-
.gsub(/^[ \t]*(?:\#{1,6}[ \t]+|>[ \t]?|[-+*][ \t]+|\d+[.)][ \t]+)/, "")
|
|
123
|
-
.gsub(/(\*\*|__|~~|`+)(.+?)\1/m, '\2')
|
|
124
|
-
.gsub(/(?<!\w)\*(\S(?:.*?\S)?)\*(?!\w)/m, '\1')
|
|
125
|
-
.gsub(/(?<!\w)_(\S(?:.*?\S)?)_(?!\w)/m, '\1')
|
|
126
|
-
.tr("|", " ") # a table row's cells; the page copies them without the pipes
|
|
127
|
-
.gsub(/\s+/, " ")
|
|
128
|
-
.strip
|
|
129
|
-
end
|
|
130
|
-
private :verify_comparison_text
|
|
131
|
-
|
|
132
|
-
def reject_verify_anchor(anchor, reason)
|
|
133
|
-
Log.warn(:turn, "verify_anchor_rejected", turn_id: anchor.turn_id, reason:)
|
|
134
|
-
nil
|
|
135
|
-
end
|
|
136
|
-
private :reject_verify_anchor
|
|
137
|
-
|
|
138
|
-
def record_verdict(turn, outcome)
|
|
139
|
-
return unless outcome == "completed" && turn.verify
|
|
140
|
-
|
|
141
|
-
answer = AnswerTail.find(turn.session.messages, turn_id: turn.id)
|
|
142
|
-
parsed = Verdict.parse(answer && (answer[:content] || answer["content"]))
|
|
143
|
-
return unless parsed
|
|
144
|
-
|
|
145
|
-
record = { anchor: turn.verify.to_h, verdict: parsed.verdict, reason: parsed.reason,
|
|
146
|
-
by: turn.origin.author, check_turn_id: turn.id, at: Time.now.iso8601(3) }
|
|
147
|
-
turn.session.verdicts = turn.session.verdicts + [record]
|
|
148
|
-
record
|
|
149
|
-
end
|
|
150
|
-
private :record_verdict
|
|
14
|
+
def build_verify_anchors = TurnStages::VerifyAnchors.new(state_dir: -> { session_state_dir })
|
|
151
15
|
end
|
|
152
16
|
end
|
|
153
17
|
end
|