samagotchi 0.154.1 → 0.156.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 +29 -1
- data/bin/chi +1 -1
- data/docs/architecture.md +12 -7
- 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 +5 -5
- data/docs/internals/attached-context.md +9 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/engine.md +642 -831
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +13 -6
- data/docs/internals/remote-sessions.md +3 -1
- 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 +4 -4
- data/docs/system-one.md +3 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/sandbox.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/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +7 -51
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/cli/parent_wait.rb +1 -1
- data/lib/samagotchi/config.rb +17 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_window.rb +3 -3
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/engine/context_judge.rb +21 -119
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +30 -80
- 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 +26 -13
- data/lib/samagotchi/engine/toolkit.rb +35 -191
- 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 +166 -98
- 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 +28 -11
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +1 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +215 -313
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +87 -49
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/llm_context_notice.rb +1 -1
- data/lib/samagotchi/llm_context_strategy.rb +7 -9
- data/lib/samagotchi/llm_context_view.rb +9 -0
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/model_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +6 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/note_command.rb +1 -2
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/plugin/sessions.rb +2 -5
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +3 -3
- data/lib/samagotchi/reminder_carry.rb +1 -1
- data/lib/samagotchi/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +3 -3
- data/lib/samagotchi/self_report.rb +7 -7
- data/lib/samagotchi/session.rb +50 -7
- data/lib/samagotchi/session_access/local.rb +7 -5
- data/lib/samagotchi/session_access/remote.rb +17 -3
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +138 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +189 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +26 -1540
- data/lib/samagotchi/session_metrics.rb +1 -1
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +3 -3
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/system_prompt.rb +37 -30
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +2 -6
- 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/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +3 -3
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +15 -7
- data/lib/samagotchi/tool_declarations.rb +6 -6
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +96 -102
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/delegate.rb +10 -5
- data/lib/samagotchi/tools/delegate_result.rb +4 -4
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/registry.rb +64 -44
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +150 -102
- data/lib/samagotchi/tools/tool_surface.rb +187 -0
- data/lib/samagotchi/turn_context.rb +68 -0
- data/lib/samagotchi/turn_stages/context_judge.rb +172 -0
- data/lib/samagotchi/turn_stages/objectives.rb +150 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +257 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +168 -0
- data/lib/samagotchi/turn_stages/warmup.rb +82 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +3 -3
- data/lib/samagotchi/web/app.rb +15 -8
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +1 -1
- data/lib/samagotchi/web/participant_access.rb +2 -2
- 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
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +3 -3
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +3 -3
- data/lib/samagotchi/web/session_summary.rb +3 -4
- data/lib/samagotchi/worker.rb +55 -27
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +29 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../config"
|
|
4
|
+
require_relative "../log"
|
|
5
|
+
require_relative "../side_judgments"
|
|
6
|
+
require_relative "../system_one/client"
|
|
7
|
+
require_relative "../toolkit"
|
|
8
|
+
require_relative "../toolkit_suggest"
|
|
9
|
+
require_relative "../tools/tool_index"
|
|
10
|
+
|
|
11
|
+
module Samagotchi
|
|
12
|
+
# The steps of a turn that keep state across its preparation, each a
|
|
13
|
+
# collaborator the Engine builds from constructor arguments and
|
|
14
|
+
# Engine#prepare_turn calls by name (docs/internals/engine.md, "Turn
|
|
15
|
+
# stages").
|
|
16
|
+
module TurnStages
|
|
17
|
+
# System 1's toolkit suggestion (ToolkitSuggest): the ask a turn starts
|
|
18
|
+
# before the probes, settles before apply_toolkit! (on mode) or abandons
|
|
19
|
+
# at a Stop or a failure, and the idle worker's warm-up. Its state is
|
|
20
|
+
# the running turn's Pending (turn thread only) and the warm-up thread.
|
|
21
|
+
class ToolkitSuggestion
|
|
22
|
+
# @param side_judgments [SideJudgments] the Engine's side asks (:toolkit kind)
|
|
23
|
+
# @param host_registry [HostRegistry] System 1's client resolves its host here
|
|
24
|
+
# @param toolkit_binding [ToolkitBinding] the kits, #suggest and #fell_back
|
|
25
|
+
# @param tools [Tools::Registry] the registered tools, for the snapshot
|
|
26
|
+
def initialize(side_judgments:, host_registry:, toolkit_binding:, tools:)
|
|
27
|
+
@side_judgments = side_judgments
|
|
28
|
+
@host_registry = host_registry
|
|
29
|
+
@toolkit_binding = toolkit_binding
|
|
30
|
+
@tools = tools
|
|
31
|
+
@pending = nil
|
|
32
|
+
@pending_turn = nil
|
|
33
|
+
@warmup_thread = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# For specs: the running warm-up (#warm!), nil when none started or it
|
|
37
|
+
# was stopped.
|
|
38
|
+
attr_reader :warmup_thread
|
|
39
|
+
|
|
40
|
+
# For specs: the ask's thread (SideJudgments' :toolkit kind), nil before any.
|
|
41
|
+
def ask_thread = @side_judgments.thread(:toolkit)
|
|
42
|
+
|
|
43
|
+
# Start the toolkit classifier after :turn_started, for audit and on
|
|
44
|
+
# mode alike (one eligibility, one snapshot, one thread). All session
|
|
45
|
+
# and tool-registry reads happen here; the thread receives only this
|
|
46
|
+
# snapshot. Audit never blocks: the thread logs its line. On mode
|
|
47
|
+
# keeps the ask's Pending for +turn+, which #settle joins before
|
|
48
|
+
# apply_toolkit!, with the turn's controller as the request's cancel
|
|
49
|
+
# controller. The suggestion log is in the turn's sessions dir
|
|
50
|
+
# (TurnContext#state_dir).
|
|
51
|
+
# @param recorder [SystemOne::Recorder] the ask's runs (built for this call)
|
|
52
|
+
# @return [ToolkitSuggest::Pending, nil] nil unless the turn joins an ask
|
|
53
|
+
def start(turn, recorder:)
|
|
54
|
+
@pending = nil
|
|
55
|
+
@pending_turn = turn
|
|
56
|
+
@pending = ask(turn, recorder)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# On mode, before apply_toolkit!: wait for +turn+'s ask (#start) up to
|
|
60
|
+
# System 1's timeout from its start, or until a Stop, and put the
|
|
61
|
+
# session on a candidate picked with confidence ≥ threshold
|
|
62
|
+
# (ToolkitBinding#suggest: origin suggested, saved, the notice). Any
|
|
63
|
+
# other outcome (the default, below threshold, undecided, no judgment,
|
|
64
|
+
# a timeout, a Stop) leaves the session on its default. Nothing here
|
|
65
|
+
# sends a request; prepare_turn's cancel check ends a stopped turn. A
|
|
66
|
+
# Stop wins over an answer already in (Pending#wait checks it first).
|
|
67
|
+
def settle(turn)
|
|
68
|
+
pending = pending_for(turn)
|
|
69
|
+
return if pending.nil?
|
|
70
|
+
|
|
71
|
+
outcome, judged = pending.wait(turn.controller)
|
|
72
|
+
if outcome == :timeout
|
|
73
|
+
# The session starts on its default without a word otherwise: a
|
|
74
|
+
# slow System 1 host (a local one) is the usual cause.
|
|
75
|
+
timeout_ms = SystemOne::Client.configured_timeout_ms
|
|
76
|
+
Log.warn(:toolkit, "toolkit_suggest_timeout", session_id: turn.session.id, timeout_ms: timeout_ms,
|
|
77
|
+
reason: "System 1 gave no answer within system_one.timeout_ms")
|
|
78
|
+
@toolkit_binding.fell_back(turn.session, "System 1 took over #{timeout_ms} ms")
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
unless outcome == :answered
|
|
82
|
+
Log.info(:toolkit, "toolkit_suggest_#{outcome}", session_id: turn.session.id)
|
|
83
|
+
return
|
|
84
|
+
end
|
|
85
|
+
return if judged.nil?
|
|
86
|
+
|
|
87
|
+
pending.record(outcome_of(turn.session, judged))
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
ToolkitSuggest.warn(e)
|
|
90
|
+
pending&.record("failed")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# +turn+ ended before it settled its ask (a Stop at the probes, a
|
|
94
|
+
# failure, an Interrupt): the ask's line says +reason+
|
|
95
|
+
# (Pending#abandon; nothing once it was settled, so prepare_turn's
|
|
96
|
+
# ensure calls it too). Nothing when +turn+ started no ask.
|
|
97
|
+
def abandon(turn, reason)
|
|
98
|
+
pending_for(turn)&.abandon(reason)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# +turn+'s suggestion is done (prepare_turn's ensure, after #abandon):
|
|
102
|
+
# its Pending and the turn are let go, so nothing of it outlives its
|
|
103
|
+
# preparation. Nothing for another turn's.
|
|
104
|
+
def finish(turn)
|
|
105
|
+
return unless @pending_turn.equal?(turn)
|
|
106
|
+
|
|
107
|
+
@pending = nil
|
|
108
|
+
@pending_turn = nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# On mode, as an idle worker starts (Worker#start_background): one ask
|
|
112
|
+
# with the real question and a stand-in prompt, on its own thread,
|
|
113
|
+
# never logged and never waited for, so System 1's server has the
|
|
114
|
+
# question's prefix cached for the first prompt. Only for a session a
|
|
115
|
+
# first prompt could be asked for: origin default, no model reply, not
|
|
116
|
+
# a delegate child or a review session, at least two candidates; and
|
|
117
|
+
# not when the worker starts on a first prompt (that ask comes at once).
|
|
118
|
+
def warm!(session)
|
|
119
|
+
return unless Config.get("toolkit.suggest.mode").to_s == "on"
|
|
120
|
+
|
|
121
|
+
return if session.nil? || session.toolkit_origin != "default" || session.delegate? || session.review?
|
|
122
|
+
return if ToolkitSuggest.model_reply?(session.messages) || !session.last_prompt.to_s.strip.empty?
|
|
123
|
+
|
|
124
|
+
default = Toolkit.of(session.toolkit)
|
|
125
|
+
kits = @toolkit_binding.kits
|
|
126
|
+
return if ToolkitSuggest.candidates(kits, default_name: default.name).size < 2
|
|
127
|
+
|
|
128
|
+
client = SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :toolkit)
|
|
129
|
+
return unless client
|
|
130
|
+
|
|
131
|
+
project = File.basename(session.project_root.to_s)
|
|
132
|
+
@warmup_thread = Thread.new do
|
|
133
|
+
ToolkitSuggest.warm(client: client, default: default, kits: kits, project: project)
|
|
134
|
+
end
|
|
135
|
+
rescue StandardError => e
|
|
136
|
+
ToolkitSuggest.warn(e)
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# A still-running warm-up, stopped: a real ask (or the worker leaving)
|
|
141
|
+
# doesn't wait behind it.
|
|
142
|
+
def stop_warmup
|
|
143
|
+
@warmup_thread&.kill
|
|
144
|
+
@warmup_thread = nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# The Pending #start kept for this very turn: another turn's (one that
|
|
150
|
+
# never reached #start) is never touched.
|
|
151
|
+
def pending_for(turn) = (@pending if @pending_turn.equal?(turn))
|
|
152
|
+
|
|
153
|
+
def ask(turn, recorder)
|
|
154
|
+
mode = Config.get("toolkit.suggest.mode").to_s
|
|
155
|
+
return if mode == "off"
|
|
156
|
+
|
|
157
|
+
joined = mode == "on"
|
|
158
|
+
session = turn.session
|
|
159
|
+
default = Toolkit.of(session.toolkit)
|
|
160
|
+
log = suggestion_log(turn.context.state_dir)
|
|
161
|
+
# The predicate checks the session first; the kits (memory kits are
|
|
162
|
+
# read from disk) and the log (read whole) only when it gets there.
|
|
163
|
+
kits = nil
|
|
164
|
+
candidates = nil
|
|
165
|
+
return unless ToolkitSuggest.eligible?(
|
|
166
|
+
session: session, prompt: turn.prompt, author: turn.origin.author,
|
|
167
|
+
candidate_count: lambda {
|
|
168
|
+
kits = @toolkit_binding.kits
|
|
169
|
+
candidates = ToolkitSuggest.candidates(kits, default_name: default.name)
|
|
170
|
+
candidates.size
|
|
171
|
+
},
|
|
172
|
+
last_suggestion: -> { ToolkitSuggest.last_suggestion(log, session.id) },
|
|
173
|
+
defer_turns: Config.get("toolkit.suggest.defer_turns"),
|
|
174
|
+
defer_tokens: Config.get("toolkit.suggest.defer_tokens")
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# One ask at a time: an eligible turn while the last one still runs
|
|
178
|
+
# goes unasked, and says so.
|
|
179
|
+
if @side_judgments.in_flight?(:toolkit)
|
|
180
|
+
Log.info(:toolkit, "toolkit_suggest_in_flight_skipped", session_id: session.id)
|
|
181
|
+
return
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
client = recorder.wrap(SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :toolkit),
|
|
185
|
+
ToolkitSuggest::JUDGMENT, session, mode: Config.get("toolkit.suggest.mode").to_s)
|
|
186
|
+
return unless client
|
|
187
|
+
|
|
188
|
+
# The warm-up's only value is the cache: the real ask never queues
|
|
189
|
+
# behind it.
|
|
190
|
+
stop_warmup
|
|
191
|
+
|
|
192
|
+
# Rules, not tool lists: this runs before await_init_tasks and
|
|
193
|
+
# apply_staged_tools!, so a first turn's plugin/MCP tools aren't
|
|
194
|
+
# registered yet; the report expands the rules (ToolkitSuggest.rules).
|
|
195
|
+
snapshots = candidates.map do |kit|
|
|
196
|
+
ToolkitSuggest::Candidate.new(name: kit.name.dup.freeze, description: kit.description.dup.freeze,
|
|
197
|
+
rules: ToolkitSuggest.rules(Toolkit.resolve_kit(kit.name, kits)))
|
|
198
|
+
end.freeze
|
|
199
|
+
default_snapshot = ToolkitSuggest::Candidate.new(name: default.name.dup.freeze, description: nil,
|
|
200
|
+
rules: ToolkitSuggest.rules(default.resolve(kits)))
|
|
201
|
+
registered = Tools::ToolSurface.for(@tools, toolkit: nil).declared
|
|
202
|
+
.reject { |entry| entry.source == Tools::ToolIndex::SOURCE }
|
|
203
|
+
core_tools, plugin_tools = registered.partition(&:core?).map { |entries| entries.map { |entry| entry.name.dup.freeze }.freeze }
|
|
204
|
+
state = ToolkitSuggest.question(
|
|
205
|
+
default: default, kits: kits, project: File.basename(session.project_root.to_s),
|
|
206
|
+
prompt: turn.prompt
|
|
207
|
+
).last
|
|
208
|
+
snapshot = ToolkitSuggest::Snapshot.new(
|
|
209
|
+
session_id: session.id.dup.freeze, turn_number: ToolkitSuggest.turn_number(session.messages),
|
|
210
|
+
default: default_snapshot, candidates: snapshots, core_tools: core_tools, plugin_tools: plugin_tools,
|
|
211
|
+
state: { project: state[:project].dup.freeze, prompt: state[:prompt].dup.freeze }.freeze
|
|
212
|
+
)
|
|
213
|
+
threshold = Config.get("toolkit.suggest.threshold")
|
|
214
|
+
runs = session.scratch ? nil : recorder.runs
|
|
215
|
+
pending = ToolkitSuggest::Pending.new(log: log, joined: joined,
|
|
216
|
+
acted: runs && ->(judged, did) { runs.acted(judged.fields[:run_id], did) },
|
|
217
|
+
timeout_seconds: SystemOne::Client.configured_timeout_ms / 1000.0)
|
|
218
|
+
# On mode: cut by a Stop while the turn waits (Pending#wait), not
|
|
219
|
+
# by one after it went on; audit's ask is never cut.
|
|
220
|
+
controller = joined ? pending.cancel_controller : nil
|
|
221
|
+
@side_judgments.start(:toolkit, warn: ToolkitSuggest.method(:warn), finish: pending.method(:finish)) do
|
|
222
|
+
ToolkitSuggest.judge(client: client, snapshot: snapshot, threshold: threshold,
|
|
223
|
+
cancel_controller: controller)
|
|
224
|
+
end
|
|
225
|
+
joined ? pending : nil
|
|
226
|
+
rescue StandardError => e
|
|
227
|
+
ToolkitSuggest.warn(e)
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# What the turn does with an answered ask, as the log line's did:
|
|
232
|
+
# suggested (switched), chosen (the toolkit isn't the default any
|
|
233
|
+
# more), unavailable (the kit is gone), defer or default.
|
|
234
|
+
def outcome_of(session, judged)
|
|
235
|
+
case judged.would
|
|
236
|
+
when "suggested"
|
|
237
|
+
return "chosen" unless session.toolkit_origin == "default"
|
|
238
|
+
return "unavailable" unless @toolkit_binding.kits.key?(judged.suggestion.kit)
|
|
239
|
+
|
|
240
|
+
switched = @toolkit_binding.suggest(session, judged.suggestion.kit, confidence: judged.suggestion.confidence,
|
|
241
|
+
deferred: ToolkitSuggest.model_reply?(session.messages))
|
|
242
|
+
switched ? "suggested" : "chosen"
|
|
243
|
+
when "defer" then "defer"
|
|
244
|
+
when "no_judgment"
|
|
245
|
+
# System 1 failed (its own warn) or answered nothing usable.
|
|
246
|
+
@toolkit_binding.fell_back(session, "System 1 gave no answer")
|
|
247
|
+
"default"
|
|
248
|
+
else "default"
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def suggestion_log(state_dir)
|
|
253
|
+
SideJudgments.log(ToolkitSuggest.log_path(state_dir), tag: :toolkit, event: "toolkit_suggest_log_failed")
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "../answer_tail"
|
|
5
|
+
require_relative "../log"
|
|
6
|
+
require_relative "../output_formatter"
|
|
7
|
+
require_relative "../redact"
|
|
8
|
+
require_relative "../revise_anchor"
|
|
9
|
+
require_relative "../session"
|
|
10
|
+
require_relative "../steer"
|
|
11
|
+
require_relative "../verdict"
|
|
12
|
+
require_relative "../verify_anchor"
|
|
13
|
+
|
|
14
|
+
module Samagotchi
|
|
15
|
+
module TurnStages
|
|
16
|
+
# Verify and Revise anchors over a turn: a Verify turn's quote checked
|
|
17
|
+
# against its source session as the turn begins; at its end (inside
|
|
18
|
+
# Engine#end_turn's event step) the opening's stamp, a Revise turn's
|
|
19
|
+
# draft checked and a Verify turn's verdict recorded. No state of its
|
|
20
|
+
# own: it reads the turn, and the sessions it loads from the state dir
|
|
21
|
+
# it is given.
|
|
22
|
+
class VerifyAnchors
|
|
23
|
+
# A Verify turn's claim (Engine#begin_turn), when its quote is in the
|
|
24
|
+
# source session's answer for that turn; nil (logged) otherwise.
|
|
25
|
+
# @param anchor [VerifyAnchor, nil]
|
|
26
|
+
# @param state_dir [String] the sessions dir (a fresh Verify child's
|
|
27
|
+
# parent is loaded from it): the Engine's at the call, as no
|
|
28
|
+
# TurnContext holds it yet
|
|
29
|
+
# @return [VerifyAnchor, nil]
|
|
30
|
+
def verified(session, anchor, state_dir:)
|
|
31
|
+
return unless anchor
|
|
32
|
+
|
|
33
|
+
source = source_session(session, anchor, state_dir)
|
|
34
|
+
return unless source
|
|
35
|
+
|
|
36
|
+
turn = AnswerTail.turn_slice(Array(source.messages), anchor.turn_id)
|
|
37
|
+
return reject_verify(anchor, :turn_missing) unless turn
|
|
38
|
+
|
|
39
|
+
reply = turn.reverse.find { |message| AnswerTail.answer?(message) }
|
|
40
|
+
return reject_verify(anchor, :reply_missing) unless reply
|
|
41
|
+
|
|
42
|
+
quote_text = comparison_text(anchor.quote)
|
|
43
|
+
quoted = !quote_text.empty? && quote_in?(quote_text, (reply[:content] || reply["content"]).to_s, source)
|
|
44
|
+
return reject_verify(anchor, :quote_missing) unless quoted
|
|
45
|
+
|
|
46
|
+
anchor
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The turn's anchors, stamped on its opening message (the prompt, or a
|
|
50
|
+
# wake turn's note) in the messages it keeps: display metadata, never
|
|
51
|
+
# in the prompt (LLM::ChatLoop#plain keeps them, no wire sends them).
|
|
52
|
+
# A Verify turn's claim however it ended; a Revise turn's draft only
|
|
53
|
+
# once it completed with an answer (#revision_target). Returns the
|
|
54
|
+
# stamped ReviseAnchor (the end event's revises), or nil.
|
|
55
|
+
def stamp_opening(messages, turn, outcome)
|
|
56
|
+
return unless messages.is_a?(Array)
|
|
57
|
+
|
|
58
|
+
revises = revision_target(messages, turn, outcome)
|
|
59
|
+
fields = { verify: turn.verify&.to_h, revises: revises&.to_h }.compact
|
|
60
|
+
return if fields.empty?
|
|
61
|
+
|
|
62
|
+
opening = messages.reverse.find do |message|
|
|
63
|
+
Steer.turn_prompt?(message) && (message[:turn_id] || message["turn_id"]).to_s == turn.id
|
|
64
|
+
end
|
|
65
|
+
return unless opening
|
|
66
|
+
|
|
67
|
+
opening.merge!(fields)
|
|
68
|
+
revises
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A completed Verify turn's verdict (Verdict.parse of its answer), added
|
|
72
|
+
# to the session's verdicts; the record (the end event's verdict), or nil.
|
|
73
|
+
def record_verdict(turn, outcome)
|
|
74
|
+
return unless outcome == "completed" && turn.verify
|
|
75
|
+
|
|
76
|
+
answer = AnswerTail.find(turn.session.messages, turn_id: turn.id)
|
|
77
|
+
parsed = Verdict.parse(answer && (answer[:content] || answer["content"]))
|
|
78
|
+
return unless parsed
|
|
79
|
+
|
|
80
|
+
record = { anchor: turn.verify.to_h, verdict: parsed.verdict, reason: parsed.reason,
|
|
81
|
+
by: turn.origin.author, check_turn_id: turn.id, at: Time.now.iso8601(3) }
|
|
82
|
+
turn.session.verdicts = turn.session.verdicts + [record]
|
|
83
|
+
record
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# The turn a Revise turn revised (D6: "this turn revised that one"):
|
|
89
|
+
# only a completed turn with an answer (AnswerTail) has revised
|
|
90
|
+
# anything. A draft no prompt in the messages carries is no target (a
|
|
91
|
+
# stale page, a rolled-back turn); one a stamped turn already revised
|
|
92
|
+
# is re-targeted to its stack's top, the newest version, which the
|
|
93
|
+
# person meant ("this draft").
|
|
94
|
+
def revision_target(messages, turn, outcome)
|
|
95
|
+
return unless turn.revises && outcome == "completed"
|
|
96
|
+
|
|
97
|
+
slice = AnswerTail.turn_slice(messages, turn.id)
|
|
98
|
+
return unless slice&.any? { |message| AnswerTail.answer?(message) }
|
|
99
|
+
|
|
100
|
+
target = turn.revises.turn_id
|
|
101
|
+
seen = [turn.id]
|
|
102
|
+
until seen.include?(target)
|
|
103
|
+
return reject_revise(turn.revises, :turn_missing) unless AnswerTail.turn_slice(messages, target)
|
|
104
|
+
|
|
105
|
+
seen << target
|
|
106
|
+
newer = messages.reverse.find do |message|
|
|
107
|
+
Steer.turn_prompt?(message) && ReviseAnchor.from(message[:revises] || message["revises"])&.turn_id == target
|
|
108
|
+
end
|
|
109
|
+
return ReviseAnchor.new(turn_id: target) unless newer
|
|
110
|
+
|
|
111
|
+
target = (newer[:turn_id] || newer["turn_id"]).to_s
|
|
112
|
+
end
|
|
113
|
+
reject_revise(turn.revises, :cycle)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def reject_revise(anchor, reason)
|
|
117
|
+
Log.warn(:turn, "revise_anchor_rejected", turn_id: anchor.turn_id, reason:)
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Whether +quote_text+ is in +answer+ as the model wrote it, or as the
|
|
122
|
+
# pages show it: a page quotes its own text, which reads the session's
|
|
123
|
+
# folder relative, the home folder as ~ and a secret shape as
|
|
124
|
+
# [redacted] (a participant's page gets it so, Redact;
|
|
125
|
+
# the owner's page rewrites its quote the same way, claim_link.js
|
|
126
|
+
# normalizeClaim, so claim links match both pages). +source+ is the
|
|
127
|
+
# session whose folder that is.
|
|
128
|
+
def quote_in?(quote_text, answer, source)
|
|
129
|
+
return true if comparison_text(answer).include?(quote_text)
|
|
130
|
+
|
|
131
|
+
viewer = Redact::Viewer.new(cwd: source.working_directory)
|
|
132
|
+
comparison_text(Redact.text(answer, viewer)).include?(quote_text)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def source_session(session, anchor, state_dir)
|
|
136
|
+
return session unless session.verify_fresh
|
|
137
|
+
|
|
138
|
+
fresh = session.verify_fresh
|
|
139
|
+
return reject_verify(anchor, :fresh_anchor_mismatch) unless fresh.anchor == anchor
|
|
140
|
+
return reject_verify(anchor, :fresh_parent_missing) unless session.parent_id
|
|
141
|
+
|
|
142
|
+
Session.load(session.parent_id, state_dir: state_dir)
|
|
143
|
+
rescue ArgumentError, SystemCallError, IOError => e
|
|
144
|
+
Log.warn(:turn, "verify_anchor_rejected", turn_id: anchor.turn_id, reason: :source_session_unavailable,
|
|
145
|
+
error: e.class.name)
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def comparison_text(text)
|
|
150
|
+
OutputFormatter.strip_markup(text)
|
|
151
|
+
.gsub(/!\[([^\]]*)\]\([^)]+\)/, '\1')
|
|
152
|
+
.gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
|
|
153
|
+
.gsub(/^[ \t]*(?:\#{1,6}[ \t]+|>[ \t]?|[-+*][ \t]+|\d+[.)][ \t]+)/, "")
|
|
154
|
+
.gsub(/(\*\*|__|~~|`+)(.+?)\1/m, '\2')
|
|
155
|
+
.gsub(/(?<!\w)\*(\S(?:.*?\S)?)\*(?!\w)/m, '\1')
|
|
156
|
+
.gsub(/(?<!\w)_(\S(?:.*?\S)?)_(?!\w)/m, '\1')
|
|
157
|
+
.tr("|", " ") # a table row's cells; the page copies them without the pipes
|
|
158
|
+
.gsub(/\s+/, " ")
|
|
159
|
+
.strip
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def reject_verify(anchor, reason)
|
|
163
|
+
Log.warn(:turn, "verify_anchor_rejected", turn_id: anchor.turn_id, reason:)
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../context_note"
|
|
4
|
+
require_relative "../log"
|
|
5
|
+
require_relative "../model_profile"
|
|
6
|
+
require_relative "../prompt_warmup"
|
|
7
|
+
|
|
8
|
+
module Samagotchi
|
|
9
|
+
module TurnStages
|
|
10
|
+
# The turn-end warm-up (PromptWarmup, cache.warmup): after a completed
|
|
11
|
+
# turn, the next turn's prompt prefilled up to its user message while
|
|
12
|
+
# the user reads. Called after Engine#complete_turn, so the context
|
|
13
|
+
# judge's asks started in the turn run first: on a shared local host the
|
|
14
|
+
# warm-up queues behind them. No state of its own: the PromptWarmup
|
|
15
|
+
# keeps its pin and thread.
|
|
16
|
+
class Warmup
|
|
17
|
+
# @param prompt_warmup [PromptWarmup] the Engine's warm-up (the kernel asks it for a slot pin)
|
|
18
|
+
# @param kernel [KernelLoop] the warm-up prompt, the last slot, warmed_up!
|
|
19
|
+
# @param turn_state [TurnState] whether steers are carried to the next turn
|
|
20
|
+
# @param model_settings [SessionModelSettings] the effective model now, the next turn's LLM context strategy
|
|
21
|
+
# @param reminder_queue [ReminderQueue] whether a reminder is due
|
|
22
|
+
# @param system_prompt [#call] (model) the system prompt (Engine#system_prompt): called last, only
|
|
23
|
+
# once every check passed, as building it has side effects (the memory index metric, the prompt notes)
|
|
24
|
+
def initialize(prompt_warmup:, kernel:, turn_state:, model_settings:, reminder_queue:, system_prompt:)
|
|
25
|
+
@prompt_warmup = prompt_warmup
|
|
26
|
+
@kernel = kernel
|
|
27
|
+
@turn_state = turn_state
|
|
28
|
+
@model_settings = model_settings
|
|
29
|
+
@reminder_queue = reminder_queue
|
|
30
|
+
@system_prompt = system_prompt
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# After a completed turn, prefill the next turn's prompt up to its user
|
|
34
|
+
# message while the user reads (PromptWarmup, cache.warmup). Only on a
|
|
35
|
+
# local llama.cpp host on the native loop, and not when the next turn's
|
|
36
|
+
# start differs anyway or starts at once: a reminder due, a continue
|
|
37
|
+
# offer (the turn ran out of steps), a carried steer or queued input.
|
|
38
|
+
# Never fails the turn. Each read comes only once the checks before it
|
|
39
|
+
# passed: the model resolved after the turn's own outcome, the probe
|
|
40
|
+
# and the prompt only on a target.
|
|
41
|
+
# @param next_turn_waiting [#call, nil] the host's probe: true when the next turn starts at once or none
|
|
42
|
+
# follows (Engine#next_turn_waiting); nil: neither
|
|
43
|
+
def after(turn, result, next_turn_waiting:)
|
|
44
|
+
return if result.canceled? || result.resumable?
|
|
45
|
+
|
|
46
|
+
model = @model_settings.effective_model
|
|
47
|
+
client = model.client
|
|
48
|
+
return unless target?(model, client)
|
|
49
|
+
return if @reminder_queue.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.call(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; the turn's settings
|
|
54
|
+
# (TurnContext, set in Engine#generate) for the rest.
|
|
55
|
+
settings = turn.context.settings
|
|
56
|
+
prompt, images = @kernel.warmup_prompt(messages, llm_context: @model_settings.turn_llm_context || settings.llm_context,
|
|
57
|
+
vision: settings.vision)
|
|
58
|
+
return unless prompt
|
|
59
|
+
|
|
60
|
+
# The model name as the native run sent it (KernelLoop#run's
|
|
61
|
+
# completion name: the default model for a blank one).
|
|
62
|
+
started = @prompt_warmup.start(client: client, prompt: prompt, model: ModelProfile.required_model_name(settings.model_name),
|
|
63
|
+
slot: @kernel.last_slot, images: images)
|
|
64
|
+
@kernel.warmed_up! if started
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
Log.warn(:model, "warmup_skipped", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Never a paid or remote host (cache.warmup auto means local only), nor
|
|
72
|
+
# the chat loop (Splash keeps its own cache; llama.cpp's chat path is
|
|
73
|
+
# not measured), nor a client that can't warm up (a spec's stub).
|
|
74
|
+
def target?(model, client)
|
|
75
|
+
return false if model.chat? || model.entry.remote?
|
|
76
|
+
return false unless client.respond_to?(:warm_up)
|
|
77
|
+
|
|
78
|
+
PromptWarmup.enabled?
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -6,7 +6,8 @@ module Samagotchi
|
|
|
6
6
|
# A session's turn as other threads see it: whether one runs, its cancel
|
|
7
7
|
# controller and event sink, the plugin steers waiting for its next
|
|
8
8
|
# boundary, whether a person's message waits for it (its caller's
|
|
9
|
-
# input_waiting: probe),
|
|
9
|
+
# input_waiting: probe), the turn's TurnContext (#context, and the last
|
|
10
|
+
# turn's, #last_context), and the activity clock the idle jobs read.
|
|
10
11
|
#
|
|
11
12
|
# The turn thread writes it (Engine#begin_turn, #release_turn); the idle
|
|
12
13
|
# scheduler, the bridge's request threads, plugin threads and the UIs
|
|
@@ -27,6 +28,8 @@ module Samagotchi
|
|
|
27
28
|
@ended = false
|
|
28
29
|
@input_waiting = nil
|
|
29
30
|
@input_waiting_warned = false
|
|
31
|
+
@context = nil
|
|
32
|
+
@last_context = nil
|
|
30
33
|
@steers = []
|
|
31
34
|
@carried_steers = []
|
|
32
35
|
end
|
|
@@ -70,12 +73,16 @@ module Samagotchi
|
|
|
70
73
|
end
|
|
71
74
|
|
|
72
75
|
# A turn begins, on the calling thread (the turn's own): running, with
|
|
73
|
-
# its controller, sink and input_waiting probe, in one step
|
|
76
|
+
# its context, controller, sink and input_waiting probe, in one step, so
|
|
77
|
+
# no reader sees a running turn with the previous turn's context.
|
|
78
|
+
# @param context [TurnContext, nil] the turn's, built before (nil: none,
|
|
79
|
+
# a spec's bare turn)
|
|
74
80
|
# @param input_waiting [#call, nil] true while a person's message waits
|
|
75
81
|
# for the turn's next boundary (Engine#input_waiting?); nil: never
|
|
76
|
-
def begin!(controller:, sink:, input_waiting: nil)
|
|
82
|
+
def begin!(controller:, sink:, context: nil, input_waiting: nil)
|
|
77
83
|
@lock.synchronize do
|
|
78
84
|
@running = true
|
|
85
|
+
@context = context
|
|
79
86
|
@controller = controller
|
|
80
87
|
@sink = sink
|
|
81
88
|
@thread = Thread.current
|
|
@@ -87,10 +94,18 @@ module Samagotchi
|
|
|
87
94
|
end
|
|
88
95
|
end
|
|
89
96
|
|
|
90
|
-
# The turn is over: not running, no controller, sink, probe or
|
|
97
|
+
# The turn is over: not running, no context, controller, sink, probe or
|
|
98
|
+
# steers. Its context stays as the last one (#last_context); one that
|
|
99
|
+
# never got its settings (a turn stopped before Engine#generate) keeps
|
|
100
|
+
# the last settings there, as the kernel kept the last turn's before.
|
|
91
101
|
# @return [Array<Hash>] the steers it never took ({text:, source:})
|
|
92
102
|
def finish!
|
|
93
103
|
@lock.synchronize do
|
|
104
|
+
if @context
|
|
105
|
+
settings = @context.settings || @last_context&.settings
|
|
106
|
+
@last_context = settings.equal?(@context.settings) ? @context : @context.with(settings: settings)
|
|
107
|
+
end
|
|
108
|
+
@context = nil
|
|
94
109
|
@running = false
|
|
95
110
|
@controller = nil
|
|
96
111
|
@sink = nil
|
|
@@ -107,6 +122,33 @@ module Samagotchi
|
|
|
107
122
|
@lock.synchronize { @ended = true }
|
|
108
123
|
end
|
|
109
124
|
|
|
125
|
+
# @return [TurnContext, nil] the running turn's context (nil between
|
|
126
|
+
# turns); its settings are nil until Engine#generate
|
|
127
|
+
def context
|
|
128
|
+
@lock.synchronize { @context }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @return [TurnContext, nil] the last finished turn's context (nil
|
|
132
|
+
# before any), kept while the next turn runs
|
|
133
|
+
def last_context
|
|
134
|
+
@lock.synchronize { @last_context }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The turn thread's new context for its running turn (Engine#generate's
|
|
138
|
+
# settings): one assignment of a frozen value. Only the turn's own
|
|
139
|
+
# thread, and only for the same turn.
|
|
140
|
+
# @param context [TurnContext]
|
|
141
|
+
# @raise [ArgumentError] from another thread, outside a turn, or for
|
|
142
|
+
# another turn's context
|
|
143
|
+
def replace_context(context)
|
|
144
|
+
@lock.synchronize do
|
|
145
|
+
raise ArgumentError, "no turn runs on this thread" unless @running && @thread.equal?(Thread.current)
|
|
146
|
+
raise ArgumentError, "not the running turn's context" unless @context.nil? || @context.id == context.id
|
|
147
|
+
|
|
148
|
+
@context = context
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
110
152
|
# @return [CancellationController, nil] the running turn's controller
|
|
111
153
|
def controller
|
|
112
154
|
@lock.synchronize { @controller }
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Samagotchi
|
|
|
24
24
|
# @param target [HostRegistry::ModelTarget]
|
|
25
25
|
# @param profile [ModelProfile, nil] the model's prompt profile (native)
|
|
26
26
|
# @param adapter [LLM::OpenAIChat, nil] the chat host's adapter
|
|
27
|
-
# @param models [Hash{String => ModelSettings}, nil]
|
|
27
|
+
# @param models [Hash{String => ModelSettings}, nil] ModelSettings.all (specs)
|
|
28
28
|
# @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
|
|
29
29
|
# @return [Answer]
|
|
30
30
|
def self.for(target, names:, profile: nil, adapter: nil, models: nil)
|
|
@@ -47,11 +47,11 @@ module Samagotchi
|
|
|
47
47
|
|
|
48
48
|
def self.configured(_target, entry, models, names)
|
|
49
49
|
models ||= begin
|
|
50
|
-
|
|
50
|
+
ModelSettings.all
|
|
51
51
|
rescue StandardError
|
|
52
52
|
{}
|
|
53
53
|
end
|
|
54
|
-
if (setting =
|
|
54
|
+
if (setting = ModelSettings.find(names, :vision, models: models))
|
|
55
55
|
return Answer.new(value: setting.value, reason: setting.value ? nil : "models: #{setting.key} sets vision: false")
|
|
56
56
|
end
|
|
57
57
|
return nil if entry.vision.nil?
|