samagotchi 0.155.0 → 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 +14 -1
- data/bin/chi +1 -1
- data/docs/architecture.md +10 -6
- data/docs/internals/anthropic.md +1 -1
- data/docs/internals/attached-context.md +3 -2
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/engine.md +625 -935
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +2 -2
- data/docs/internals/prompt-caching.md +4 -1
- data/docs/internals/remote-sessions.md +3 -1
- data/docs/internals/working-set.md +1 -1
- data/docs/sessions.md +2 -2
- data/docs/system-one.md +3 -2
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- 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 +19 -11
- data/lib/samagotchi/engine/messages.rb +1 -1
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +29 -11
- data/lib/samagotchi/engine/side_requests.rb +21 -11
- data/lib/samagotchi/engine/toolkit.rb +10 -9
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +7 -7
- data/lib/samagotchi/engine/verify.rb +1 -1
- data/lib/samagotchi/engine.rb +14 -9
- data/lib/samagotchi/guardrails/targets.rb +24 -9
- 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/kernel_loop.rb +46 -121
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/chat_loop.rb +34 -18
- data/lib/samagotchi/llm/native_backend.rb +1 -1
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- 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_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/sessions.rb +2 -5
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.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/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/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +2 -6
- 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 +10 -4
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_runner.rb +7 -4
- data/lib/samagotchi/toolkit_command.rb +1 -1
- 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 +28 -11
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +52 -51
- data/lib/samagotchi/tools/tool_surface.rb +22 -8
- data/lib/samagotchi/turn_context.rb +2 -1
- data/lib/samagotchi/turn_stages/context_judge.rb +38 -36
- data/lib/samagotchi/turn_stages/objectives.rb +56 -52
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +13 -17
- data/lib/samagotchi/turn_stages/verify_anchors.rb +9 -10
- data/lib/samagotchi/turn_stages/warmup.rb +14 -15
- 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/participant_access.rb +2 -2
- 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 +13 -1
|
@@ -14,78 +14,62 @@ module Samagotchi
|
|
|
14
14
|
# audit of what comes in at a turn's start and what goes out at its end
|
|
15
15
|
# (ObjectivesAudit), the view announced when it changed, and the model's
|
|
16
16
|
# proposals kept as cards until the turn is over. Its state is those
|
|
17
|
-
# proposals under their mutex
|
|
17
|
+
# proposals under their mutex and the last view announced (under the
|
|
18
|
+
# event lock); the audit is built per call.
|
|
18
19
|
class Objectives
|
|
19
20
|
# @param side_judgments [SideJudgments] the Engine's side asks (the audit's kinds)
|
|
20
21
|
# @param host_registry [HostRegistry] System 1's client resolves its host here
|
|
21
|
-
|
|
22
|
-
# @param recording [#call] (client, judgment, session, mode:) → the client saving its runs
|
|
23
|
-
# @param runs [#call] the Engine's SystemOne::Runs
|
|
24
|
-
# @param synchronize [#call] (&block) runs the block under the event lock (Engine#synchronize_events)
|
|
25
|
-
# @param announce [#call] (event) tells the observers (Engine#announce)
|
|
26
|
-
# @param running [#call] the running turn's conversation, nil between turns (Engine#live_conversation)
|
|
27
|
-
# @param show_card [#call] (**card) shows a card (Engine#show_card)
|
|
28
|
-
def initialize(side_judgments:, host_registry:, state_dir:, recording:, runs:, synchronize:, announce:, running:, show_card:)
|
|
22
|
+
def initialize(side_judgments:, host_registry:)
|
|
29
23
|
@side_judgments = side_judgments
|
|
30
|
-
@
|
|
31
|
-
@synchronize = synchronize
|
|
32
|
-
@announce = announce
|
|
33
|
-
@running = running
|
|
34
|
-
@show_card = show_card
|
|
24
|
+
@host_registry = host_registry
|
|
35
25
|
# The last view announced (#announce), under the event lock.
|
|
36
26
|
@announced = nil
|
|
37
27
|
# The model's proposals until the turn is over (#finally).
|
|
38
28
|
@proposals = []
|
|
39
29
|
@proposals_mutex = Mutex.new
|
|
40
|
-
# With saving off nothing would come of an ask, so none is made.
|
|
41
|
-
@audit = ObjectivesAudit.new(
|
|
42
|
-
side: side_judgments, state_dir: state_dir,
|
|
43
|
-
client_for: lambda do |judgment, session|
|
|
44
|
-
next nil unless runs.call.on?
|
|
45
|
-
|
|
46
|
-
recording.call(SystemOne::Client.from_config(host_registry: host_registry, mode: ObjectivesAudit::MODE, tag: :system_one),
|
|
47
|
-
judgment, session, mode: nil)
|
|
48
|
-
end
|
|
49
|
-
)
|
|
50
30
|
end
|
|
51
31
|
|
|
52
32
|
# System 1 on the objectives, in audit (ObjectivesAudit), at a turn's
|
|
53
33
|
# start: the owner's turns only, and for a message a person's: a
|
|
54
34
|
# reminder's or a delegate's prompt isn't one.
|
|
55
|
-
|
|
35
|
+
# @param recorder [SystemOne::Recorder] the audit's runs (built for this call)
|
|
36
|
+
def incoming(turn, recorder:)
|
|
56
37
|
return unless turn.prompt.is_a?(String) && turn.origin.human? && OwnerPrompt.turn?(session: turn.session, author: turn.origin.author)
|
|
57
38
|
|
|
58
|
-
|
|
39
|
+
audit(turn.context.state_dir, recorder).incoming(turn.session, turn.prompt)
|
|
59
40
|
end
|
|
60
41
|
|
|
61
42
|
# The turn's answer, after the end event and before :after_turn (the
|
|
62
43
|
# caller skips a canceled or exhausted turn): the owner's turns only.
|
|
63
|
-
|
|
44
|
+
# @param recorder [SystemOne::Recorder] the audit's runs (built for this
|
|
45
|
+
# call: after a before_turn hook's model switch, the new model)
|
|
46
|
+
def outgoing(turn, result, recorder:)
|
|
64
47
|
return unless OwnerPrompt.turn?(session: turn.session, author: turn.origin.author)
|
|
65
48
|
|
|
66
|
-
|
|
49
|
+
audit(turn.context.state_dir, recorder).outgoing(turn.session, result.text)
|
|
67
50
|
end
|
|
68
51
|
|
|
69
52
|
# A note added between turns (Engine#add_context_note): incoming too.
|
|
70
|
-
|
|
53
|
+
# @param recorder [SystemOne::Recorder] the audit's runs (built for this call)
|
|
54
|
+
# @param state_dir [String] the sessions dir
|
|
55
|
+
def note(session, note, recorder:, state_dir:) = audit(state_dir, recorder).note(session, note)
|
|
71
56
|
|
|
72
57
|
# The session's objectives as the page draws them (Objectives.view), when
|
|
73
58
|
# that differs from the last announced: after an edit (its file written,
|
|
74
59
|
# pending), once a list note is in, at a turn's end (its moves) and after
|
|
75
|
-
# a rollback. A session with no list announces nothing.
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
Log.warn(:turn, "objectives_announce_failed", error: e.class.name, msg: e.message)
|
|
60
|
+
# a rollback. A session with no list announces nothing. The caller
|
|
61
|
+
# (Engine#announce_objectives) holds the event lock, the one guard of
|
|
62
|
+
# the last view, announces the event and logs a failure: it only
|
|
63
|
+
# informs, never fails a turn.
|
|
64
|
+
# @param state_dir [String] the sessions dir
|
|
65
|
+
# @param running [Array<Hash>, nil] the running turn's conversation (Engine#live_conversation), nil between turns
|
|
66
|
+
# @return [Hash, nil] the :objectives_updated event to announce, nil when the view is the same
|
|
67
|
+
def announce(session, state_dir:, running:)
|
|
68
|
+
view = Samagotchi::Objectives.view(session.id, session.messages, state_dir: state_dir, running: running&.dup)
|
|
69
|
+
return if view.nil? || view == @announced
|
|
70
|
+
|
|
71
|
+
@announced = view
|
|
72
|
+
{ type: :objectives_updated, session_id: session.id, **view }
|
|
89
73
|
end
|
|
90
74
|
|
|
91
75
|
# A change the model proposes (the objectives tool), kept until the turn
|
|
@@ -108,9 +92,15 @@ module Samagotchi
|
|
|
108
92
|
# failed or rolled-back turn keeps none, and its proposals are dropped
|
|
109
93
|
# here so the next turn does not show them either: their Apply card
|
|
110
94
|
# would sit after a turn whose `objectives propose` call is not in the
|
|
111
|
-
# history (a rollback after Ctrl-C drops it).
|
|
95
|
+
# history (a rollback after Ctrl-C drops it). The caller
|
|
96
|
+
# (Engine#show_objective_proposals) shows the cards.
|
|
97
|
+
# @return [Array<Hash>] the completed turn's cards (Engine#show_card's
|
|
98
|
+
# arguments); none for any other ending
|
|
112
99
|
def finally(turn)
|
|
113
|
-
turn.completed?
|
|
100
|
+
return proposal_cards if turn.completed?
|
|
101
|
+
|
|
102
|
+
discard_proposals(turn)
|
|
103
|
+
[]
|
|
114
104
|
end
|
|
115
105
|
|
|
116
106
|
# For specs: the audit's asks' thread for +kind+ (ObjectivesAudit's
|
|
@@ -122,15 +112,13 @@ module Samagotchi
|
|
|
122
112
|
|
|
123
113
|
private
|
|
124
114
|
|
|
125
|
-
def
|
|
126
|
-
drain_proposals.
|
|
115
|
+
def proposal_cards
|
|
116
|
+
drain_proposals.map do |proposal|
|
|
127
117
|
id = "objectives-#{SecureRandom.hex(4)}"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
118
|
+
{ source: Samagotchi::Objectives::SOURCE, id: id, title: proposal.title, body: proposal.body,
|
|
119
|
+
actions: [{ label: "Apply", command: proposal.action_line(id) },
|
|
120
|
+
{ label: "Dismiss", command: "/objectives --card #{id} dismiss" }] }
|
|
131
121
|
end
|
|
132
|
-
rescue StandardError => e
|
|
133
|
-
Log.warn(:turn, "objectives_proposal_failed", error: e.class.name, msg: e.message)
|
|
134
122
|
end
|
|
135
123
|
|
|
136
124
|
def discard_proposals(turn)
|
|
@@ -141,6 +129,22 @@ module Samagotchi
|
|
|
141
129
|
def drain_proposals
|
|
142
130
|
@proposals_mutex.synchronize { @proposals.slice!(0..) }
|
|
143
131
|
end
|
|
132
|
+
|
|
133
|
+
# The audit for one call, over the values it is given: nothing of it
|
|
134
|
+
# is kept. Its client is asked for only once an active objective is
|
|
135
|
+
# there (ObjectivesAudit#ask), and with saving off nothing would come
|
|
136
|
+
# of an ask, so none is made.
|
|
137
|
+
def audit(state_dir, recorder)
|
|
138
|
+
ObjectivesAudit.new(
|
|
139
|
+
side: @side_judgments, state_dir: state_dir,
|
|
140
|
+
client_for: lambda do |judgment, session|
|
|
141
|
+
next nil unless recorder.saving?
|
|
142
|
+
|
|
143
|
+
recorder.wrap(SystemOne::Client.from_config(host_registry: @host_registry, mode: ObjectivesAudit::MODE, tag: :system_one),
|
|
144
|
+
judgment, session, mode: nil)
|
|
145
|
+
end
|
|
146
|
+
)
|
|
147
|
+
end
|
|
144
148
|
end
|
|
145
149
|
end
|
|
146
150
|
end
|
|
@@ -23,17 +23,11 @@ module Samagotchi
|
|
|
23
23
|
# @param host_registry [HostRegistry] System 1's client resolves its host here
|
|
24
24
|
# @param toolkit_binding [ToolkitBinding] the kits, #suggest and #fell_back
|
|
25
25
|
# @param tools [Tools::Registry] the registered tools, for the snapshot
|
|
26
|
-
|
|
27
|
-
# @param recording [#call] (client, judgment, session, mode:) → the client saving its runs
|
|
28
|
-
# @param runs [#call] the Engine's SystemOne::Runs
|
|
29
|
-
def initialize(side_judgments:, host_registry:, toolkit_binding:, tools:, state_dir:, recording:, runs:)
|
|
26
|
+
def initialize(side_judgments:, host_registry:, toolkit_binding:, tools:)
|
|
30
27
|
@side_judgments = side_judgments
|
|
31
28
|
@host_registry = host_registry
|
|
32
29
|
@toolkit_binding = toolkit_binding
|
|
33
30
|
@tools = tools
|
|
34
|
-
@state_dir = state_dir
|
|
35
|
-
@recording = recording
|
|
36
|
-
@runs = runs
|
|
37
31
|
@pending = nil
|
|
38
32
|
@pending_turn = nil
|
|
39
33
|
@warmup_thread = nil
|
|
@@ -52,12 +46,14 @@ module Samagotchi
|
|
|
52
46
|
# snapshot. Audit never blocks: the thread logs its line. On mode
|
|
53
47
|
# keeps the ask's Pending for +turn+, which #settle joins before
|
|
54
48
|
# apply_toolkit!, with the turn's controller as the request's cancel
|
|
55
|
-
# controller.
|
|
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)
|
|
56
52
|
# @return [ToolkitSuggest::Pending, nil] nil unless the turn joins an ask
|
|
57
|
-
def start(turn)
|
|
53
|
+
def start(turn, recorder:)
|
|
58
54
|
@pending = nil
|
|
59
55
|
@pending_turn = turn
|
|
60
|
-
@pending = ask(turn)
|
|
56
|
+
@pending = ask(turn, recorder)
|
|
61
57
|
end
|
|
62
58
|
|
|
63
59
|
# On mode, before apply_toolkit!: wait for +turn+'s ask (#start) up to
|
|
@@ -154,14 +150,14 @@ module Samagotchi
|
|
|
154
150
|
# never reached #start) is never touched.
|
|
155
151
|
def pending_for(turn) = (@pending if @pending_turn.equal?(turn))
|
|
156
152
|
|
|
157
|
-
def ask(turn)
|
|
153
|
+
def ask(turn, recorder)
|
|
158
154
|
mode = Config.get("toolkit.suggest.mode").to_s
|
|
159
155
|
return if mode == "off"
|
|
160
156
|
|
|
161
157
|
joined = mode == "on"
|
|
162
158
|
session = turn.session
|
|
163
159
|
default = Toolkit.of(session.toolkit)
|
|
164
|
-
log = suggestion_log
|
|
160
|
+
log = suggestion_log(turn.context.state_dir)
|
|
165
161
|
# The predicate checks the session first; the kits (memory kits are
|
|
166
162
|
# read from disk) and the log (read whole) only when it gets there.
|
|
167
163
|
kits = nil
|
|
@@ -185,8 +181,8 @@ module Samagotchi
|
|
|
185
181
|
return
|
|
186
182
|
end
|
|
187
183
|
|
|
188
|
-
client =
|
|
189
|
-
|
|
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)
|
|
190
186
|
return unless client
|
|
191
187
|
|
|
192
188
|
# The warm-up's only value is the cache: the real ask never queues
|
|
@@ -215,7 +211,7 @@ module Samagotchi
|
|
|
215
211
|
state: { project: state[:project].dup.freeze, prompt: state[:prompt].dup.freeze }.freeze
|
|
216
212
|
)
|
|
217
213
|
threshold = Config.get("toolkit.suggest.threshold")
|
|
218
|
-
runs = session.scratch ? nil :
|
|
214
|
+
runs = session.scratch ? nil : recorder.runs
|
|
219
215
|
pending = ToolkitSuggest::Pending.new(log: log, joined: joined,
|
|
220
216
|
acted: runs && ->(judged, did) { runs.acted(judged.fields[:run_id], did) },
|
|
221
217
|
timeout_seconds: SystemOne::Client.configured_timeout_ms / 1000.0)
|
|
@@ -253,8 +249,8 @@ module Samagotchi
|
|
|
253
249
|
end
|
|
254
250
|
end
|
|
255
251
|
|
|
256
|
-
def suggestion_log
|
|
257
|
-
SideJudgments.log(ToolkitSuggest.log_path(
|
|
252
|
+
def suggestion_log(state_dir)
|
|
253
|
+
SideJudgments.log(ToolkitSuggest.log_path(state_dir), tag: :toolkit, event: "toolkit_suggest_log_failed")
|
|
258
254
|
end
|
|
259
255
|
end
|
|
260
256
|
end
|
|
@@ -17,21 +17,20 @@ module Samagotchi
|
|
|
17
17
|
# against its source session as the turn begins; at its end (inside
|
|
18
18
|
# Engine#end_turn's event step) the opening's stamp, a Revise turn's
|
|
19
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
|
|
20
|
+
# own: it reads the turn, and the sessions it loads from the state dir
|
|
21
|
+
# it is given.
|
|
21
22
|
class VerifyAnchors
|
|
22
|
-
# @param state_dir [#call] the sessions dir (a fresh Verify child's parent is loaded from it)
|
|
23
|
-
def initialize(state_dir:)
|
|
24
|
-
@state_dir = state_dir
|
|
25
|
-
end
|
|
26
|
-
|
|
27
23
|
# A Verify turn's claim (Engine#begin_turn), when its quote is in the
|
|
28
24
|
# source session's answer for that turn; nil (logged) otherwise.
|
|
29
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
|
|
30
29
|
# @return [VerifyAnchor, nil]
|
|
31
|
-
def verified(session, anchor)
|
|
30
|
+
def verified(session, anchor, state_dir:)
|
|
32
31
|
return unless anchor
|
|
33
32
|
|
|
34
|
-
source = source_session(session, anchor)
|
|
33
|
+
source = source_session(session, anchor, state_dir)
|
|
35
34
|
return unless source
|
|
36
35
|
|
|
37
36
|
turn = AnswerTail.turn_slice(Array(source.messages), anchor.turn_id)
|
|
@@ -133,14 +132,14 @@ module Samagotchi
|
|
|
133
132
|
comparison_text(Redact.text(answer, viewer)).include?(quote_text)
|
|
134
133
|
end
|
|
135
134
|
|
|
136
|
-
def source_session(session, anchor)
|
|
135
|
+
def source_session(session, anchor, state_dir)
|
|
137
136
|
return session unless session.verify_fresh
|
|
138
137
|
|
|
139
138
|
fresh = session.verify_fresh
|
|
140
139
|
return reject_verify(anchor, :fresh_anchor_mismatch) unless fresh.anchor == anchor
|
|
141
140
|
return reject_verify(anchor, :fresh_parent_missing) unless session.parent_id
|
|
142
141
|
|
|
143
|
-
Session.load(session.parent_id, state_dir:
|
|
142
|
+
Session.load(session.parent_id, state_dir: state_dir)
|
|
144
143
|
rescue ArgumentError, SystemCallError, IOError => e
|
|
145
144
|
Log.warn(:turn, "verify_anchor_rejected", turn_id: anchor.turn_id, reason: :source_session_unavailable,
|
|
146
145
|
error: e.class.name)
|
|
@@ -17,22 +17,17 @@ module Samagotchi
|
|
|
17
17
|
# @param prompt_warmup [PromptWarmup] the Engine's warm-up (the kernel asks it for a slot pin)
|
|
18
18
|
# @param kernel [KernelLoop] the warm-up prompt, the last slot, warmed_up!
|
|
19
19
|
# @param turn_state [TurnState] whether steers are carried to the next turn
|
|
20
|
-
# @param model_settings [SessionModelSettings] the next turn's LLM context strategy
|
|
21
|
-
# @param
|
|
22
|
-
# @param system_prompt [#call] (model) the system prompt (Engine#system_prompt)
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
# (Engine#next_turn_waiting, called)
|
|
26
|
-
def initialize(prompt_warmup:, kernel:, turn_state:, model_settings:, model:, system_prompt:, reminders_due:,
|
|
27
|
-
next_turn_waiting:)
|
|
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:)
|
|
28
25
|
@prompt_warmup = prompt_warmup
|
|
29
26
|
@kernel = kernel
|
|
30
27
|
@turn_state = turn_state
|
|
31
28
|
@model_settings = model_settings
|
|
32
|
-
@
|
|
29
|
+
@reminder_queue = reminder_queue
|
|
33
30
|
@system_prompt = system_prompt
|
|
34
|
-
@reminders_due = reminders_due
|
|
35
|
-
@next_turn_waiting = next_turn_waiting
|
|
36
31
|
end
|
|
37
32
|
|
|
38
33
|
# After a completed turn, prefill the next turn's prompt up to its user
|
|
@@ -40,14 +35,18 @@ module Samagotchi
|
|
|
40
35
|
# local llama.cpp host on the native loop, and not when the next turn's
|
|
41
36
|
# start differs anyway or starts at once: a reminder due, a continue
|
|
42
37
|
# offer (the turn ran out of steps), a carried steer or queued input.
|
|
43
|
-
# Never fails the turn.
|
|
44
|
-
|
|
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:)
|
|
45
44
|
return if result.canceled? || result.resumable?
|
|
46
45
|
|
|
47
|
-
model = @
|
|
46
|
+
model = @model_settings.effective_model
|
|
48
47
|
client = model.client
|
|
49
48
|
return unless target?(model, client)
|
|
50
|
-
return if @
|
|
49
|
+
return if @reminder_queue.due? || @turn_state.carried_steers? || next_turn_waiting&.call
|
|
51
50
|
|
|
52
51
|
messages = ContextNote.with_system_head(turn.session.messages.dup, { role: "system", content: @system_prompt.call(model) })
|
|
53
52
|
# Under the next turn's strategy (a /model switch may have changed it);
|
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?
|
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -353,10 +353,10 @@ module Samagotchi
|
|
|
353
353
|
# "..", a %2F) is an unknown session before any handler makes a path.
|
|
354
354
|
# (/api/sessions/resolve and /summaries take no id.)
|
|
355
355
|
if req.path_info.start_with?("/api/sessions/") && !m.captures.empty? && !Session.valid_id?(m.captures.first)
|
|
356
|
-
return
|
|
356
|
+
return session_not_found(m.captures.first)
|
|
357
357
|
end
|
|
358
358
|
|
|
359
|
-
return session_route(principal) { send(handler, req, *m.captures) } if kind == :session
|
|
359
|
+
return session_route(principal, m.captures.first) { send(handler, req, *m.captures) } if kind == :session
|
|
360
360
|
|
|
361
361
|
return send(handler, req, *m.captures)
|
|
362
362
|
end
|
|
@@ -383,11 +383,14 @@ module Samagotchi
|
|
|
383
383
|
# raises (Session.load, the manager's find) is a 404 not_found with
|
|
384
384
|
# its message. Only these routes: elsewhere an ArgumentError is a bug,
|
|
385
385
|
# #route's 500. A handler's own rescues run first.
|
|
386
|
-
def session_route(principal = nil)
|
|
386
|
+
def session_route(principal = nil, id = nil)
|
|
387
387
|
yield
|
|
388
388
|
rescue ArgumentError => e
|
|
389
|
-
|
|
390
|
-
|
|
389
|
+
return error_response(404, "not_found", e.message) if principal.nil? || principal.owner?
|
|
390
|
+
|
|
391
|
+
# A participant's 404 never carries a lookup's words (a path): it
|
|
392
|
+
# names their own session's id (valid_id? passed), nothing else.
|
|
393
|
+
session_not_found(id)
|
|
391
394
|
end
|
|
392
395
|
|
|
393
396
|
# API requests at debug level (not the page and its assets): method,
|
|
@@ -1152,7 +1155,7 @@ module Samagotchi
|
|
|
1152
1155
|
Degrade.call(:web, "model_sampling_unreadable") do
|
|
1153
1156
|
return if registry.nil? || models.empty?
|
|
1154
1157
|
|
|
1155
|
-
settings =
|
|
1158
|
+
settings = ModelSettings.all
|
|
1156
1159
|
models.each do |model|
|
|
1157
1160
|
target = registry.resolve(model[:name])
|
|
1158
1161
|
names = registry.lookup_names(model[:name], target: target)
|
|
@@ -1214,7 +1217,7 @@ module Samagotchi
|
|
|
1214
1217
|
@models_mutex.synchronize do
|
|
1215
1218
|
next @registry if @registry_given
|
|
1216
1219
|
|
|
1217
|
-
hosts =
|
|
1220
|
+
hosts = HostConfig.all
|
|
1218
1221
|
if @registry.nil? || hosts != @registry_hosts
|
|
1219
1222
|
@registry = HostRegistry.new(hosts_config: hosts)
|
|
1220
1223
|
@registry_hosts = hosts
|
|
@@ -1560,7 +1563,7 @@ module Samagotchi
|
|
|
1560
1563
|
begin
|
|
1561
1564
|
session = @session_class.load(id, state_dir: default_state_dir)
|
|
1562
1565
|
rescue ArgumentError
|
|
1563
|
-
return
|
|
1566
|
+
return session_not_found(id)
|
|
1564
1567
|
end
|
|
1565
1568
|
# Its terminal's alone: a tab's stream would keep its worker up.
|
|
1566
1569
|
return scratch_refused(id) if session.scratch
|
|
@@ -1976,6 +1979,10 @@ module Samagotchi
|
|
|
1976
1979
|
error_response(404, "not_found", "not found: #{path}")
|
|
1977
1980
|
end
|
|
1978
1981
|
|
|
1982
|
+
# 404 not_found for an id (or prefix) that names no session, in
|
|
1983
|
+
# Session::NotFound's words (SessionAccess::Remote maps it back).
|
|
1984
|
+
def session_not_found(given) = error_response(404, "not_found", Session::NotFound.new(given).message)
|
|
1985
|
+
|
|
1979
1986
|
# One tab's GET /api/events: a bounded queue behind a lambda sink,
|
|
1980
1987
|
# subscribed and snapshotted as one step under the hub's lock, then
|
|
1981
1988
|
# drained onto the socket. The loop ends when the hub stops or the
|
|
@@ -185,7 +185,7 @@ module Samagotchi
|
|
|
185
185
|
end
|
|
186
186
|
|
|
187
187
|
def unknown_session(id)
|
|
188
|
-
|
|
188
|
+
session_not_found(id)
|
|
189
189
|
end
|
|
190
190
|
|
|
191
191
|
# 403 not_permitted for a participant (the owner is never refused by
|
|
@@ -388,7 +388,7 @@ module Samagotchi
|
|
|
388
388
|
def participant_resolve(grant, given)
|
|
389
389
|
return json_response(200, { id: grant.session_id }) if grant.session_id.start_with?(given)
|
|
390
390
|
|
|
391
|
-
|
|
391
|
+
session_not_found(given)
|
|
392
392
|
end
|
|
393
393
|
|
|
394
394
|
# POST …/wait/prepare for a participant (an agent's chi --remote
|
|
@@ -78,14 +78,14 @@ module Samagotchi
|
|
|
78
78
|
# 404 not_found; 400 ambiguous_id with ids (the sessions it names).
|
|
79
79
|
def handle_resolve(req)
|
|
80
80
|
given = req.params["id"].to_s
|
|
81
|
-
return
|
|
81
|
+
return session_not_found(given) unless Session.valid_id?(given)
|
|
82
82
|
return participant_resolve(principal(req).grant, given) unless principal(req).owner?
|
|
83
83
|
|
|
84
84
|
json_response(200, { id: session_access.resolve(given) })
|
|
85
85
|
rescue Session::AmbiguousId => e
|
|
86
86
|
refusal_response(400, "ambiguous_id", e, ids: e.ids)
|
|
87
|
-
rescue ArgumentError
|
|
88
|
-
|
|
87
|
+
rescue ArgumentError # NotFound, a file that won't load
|
|
88
|
+
session_not_found(given)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
# GET /api/sessions/:id?brief=1: the session as a session command
|
|
@@ -47,7 +47,7 @@ module Samagotchi
|
|
|
47
47
|
|
|
48
48
|
def handle_question_answer(req, id)
|
|
49
49
|
session = @session_class.load(id, state_dir: default_state_dir) rescue nil
|
|
50
|
-
return
|
|
50
|
+
return session_not_found(id) unless session
|
|
51
51
|
|
|
52
52
|
body = parse_json(request_body(req))
|
|
53
53
|
return error_response(400, "invalid_json", "invalid JSON body") unless body.is_a?(Hash)
|
|
@@ -239,7 +239,7 @@ module Samagotchi
|
|
|
239
239
|
return [] if raw.nil?
|
|
240
240
|
|
|
241
241
|
session_dir = image_session_dir(id)
|
|
242
|
-
return
|
|
242
|
+
return Session::NotFound.new(id).message unless session_dir
|
|
243
243
|
|
|
244
244
|
ImageStore.check_refs(session_dir, raw)
|
|
245
245
|
end
|
|
@@ -259,7 +259,7 @@ module Samagotchi
|
|
|
259
259
|
# (converted, downscaled); answers the ref the turn then names.
|
|
260
260
|
def handle_image_upload(req, id)
|
|
261
261
|
session_dir = image_session_dir(id)
|
|
262
|
-
return
|
|
262
|
+
return session_not_found(id) unless session_dir
|
|
263
263
|
|
|
264
264
|
length = req.content_length.to_i
|
|
265
265
|
if length > MAX_IMAGE_UPLOAD_BYTES
|
|
@@ -140,7 +140,7 @@ module Samagotchi
|
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
# POST /api/sessions with continues: <id, prefix or last:id>: the next
|
|
143
|
-
# link of that session's chain (
|
|
143
|
+
# link of that session's chain (SessionContinue.run), in its
|
|
144
144
|
# folder and with its own setup (SessionSetup: llm_context, thinking
|
|
145
145
|
# and reasoning_history levels, settings), so dir, llm_context, thinking and settings are
|
|
146
146
|
# refused beside it (a create takes no reasoning_history). It runs on
|
|
@@ -328,11 +328,11 @@ module Samagotchi
|
|
|
328
328
|
[level, nil]
|
|
329
329
|
end
|
|
330
330
|
|
|
331
|
-
#
|
|
331
|
+
# SessionContinue.run, its refusals as responses: [session,
|
|
332
332
|
# nil] or [nil, the response]. Only its own errors: what fails after
|
|
333
333
|
# the new link started is not a refusal of it.
|
|
334
334
|
def continue_from(given, prompt:, preview:, model:)
|
|
335
|
-
[
|
|
335
|
+
[SessionContinue.run(given, prompt: prompt, preview: preview, model: model, state_dir: @state_dir), nil]
|
|
336
336
|
rescue SessionContinue::Refused => e
|
|
337
337
|
extra = e.reason == :continued ? { next_id: e.ids.first } : {}
|
|
338
338
|
[nil, refusal_response(409, e.reason.to_s, e, session_id: e.session_id, reason: e.reason.to_s, ids: e.ids, **extra)]
|
|
@@ -412,7 +412,7 @@ module Samagotchi
|
|
|
412
412
|
# read is mtime-cached), so a host added or a window changed while
|
|
413
413
|
# chi web runs is picked up on the next refresh of a card.
|
|
414
414
|
def hub_registry
|
|
415
|
-
hosts =
|
|
415
|
+
hosts = HostConfig.all
|
|
416
416
|
if @hub_registry.nil? || hosts != @hub_registry_hosts
|
|
417
417
|
@hub_registry = HostRegistry.new(hosts_config: hosts)
|
|
418
418
|
@hub_registry_hosts = hosts
|
|
@@ -184,7 +184,7 @@ module Samagotchi
|
|
|
184
184
|
# cards from its light GET tail, else the saved ones; the session file
|
|
185
185
|
# is only stat'ed (the 404), never parsed.
|
|
186
186
|
def handle_cards_read(id)
|
|
187
|
-
return
|
|
187
|
+
return session_not_found(id) unless session_exists?(id)
|
|
188
188
|
|
|
189
189
|
tail = live_tail(id)
|
|
190
190
|
json_response(200, { cards: cards_for_display(live_or_saved(tail, "cards") { saved_cards(id) }) })
|
|
@@ -201,7 +201,7 @@ module Samagotchi
|
|
|
201
201
|
# are the newest turn's (#recent_timing), which the page merges by id
|
|
202
202
|
# (the page still sends ?recent=1, which is accepted and not needed).
|
|
203
203
|
def handle_tail_read(req, id)
|
|
204
|
-
return
|
|
204
|
+
return session_not_found(id) unless session_exists?(id)
|
|
205
205
|
|
|
206
206
|
turn_id = req.params["turn_id"].to_s.strip
|
|
207
207
|
turn_id = nil if turn_id.empty?
|
|
@@ -259,7 +259,7 @@ module Samagotchi
|
|
|
259
259
|
# the live ones) and turn_count, when the page's merge comes up short.
|
|
260
260
|
# The worker's GET state, never its messages.
|
|
261
261
|
def handle_timing_read(id)
|
|
262
|
-
return
|
|
262
|
+
return session_not_found(id) unless session_exists?(id)
|
|
263
263
|
|
|
264
264
|
state = bridge_get_json(id, "state")
|
|
265
265
|
json_response(200, { timing: timing_payload(id, live_metrics: state && state["session_state_snapshot"]&.fetch("metrics", nil)) })
|
|
@@ -146,13 +146,12 @@ module Samagotchi
|
|
|
146
146
|
private_class_method :budget_resolver
|
|
147
147
|
|
|
148
148
|
# status is turn state (idle/running). The live worker's snapshot is the
|
|
149
|
-
# truth; on disk, a "running" with no live
|
|
150
|
-
# that died mid-turn.
|
|
149
|
+
# truth; on disk, Session#displayed_status (a "running" with no live
|
|
150
|
+
# owner was left by a worker that died mid-turn).
|
|
151
151
|
def displayed_status(session, snapshot = nil, owner:)
|
|
152
152
|
return snapshot["status"] if snapshot.is_a?(Hash) && snapshot["status"]
|
|
153
|
-
return session.status unless session.status == Session::STATUS_RUNNING
|
|
154
153
|
|
|
155
|
-
|
|
154
|
+
session.displayed_status(owner: owner)
|
|
156
155
|
end
|
|
157
156
|
|
|
158
157
|
def first_preview_for(session)
|