samagotchi 0.125.0 → 0.127.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 +50 -1
- data/docs/architecture.md +1 -0
- data/docs/cli.md +1 -1
- data/docs/configuration.md +2 -2
- data/docs/guardrails.md +22 -0
- data/docs/internals/participants.md +82 -8
- data/docs/internals/web-push.md +10 -6
- data/docs/sessions.md +6 -1
- data/lib/samagotchi/bridge.rb +4 -3
- data/lib/samagotchi/bridge_client.rb +8 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +25 -4
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +3 -0
- data/lib/samagotchi/children_status.rb +6 -1
- data/lib/samagotchi/engine.rb +43 -7
- data/lib/samagotchi/guardrail_wiring.rb +10 -1
- data/lib/samagotchi/llm/chat_loop.rb +1 -0
- data/lib/samagotchi/session.rb +40 -3
- data/lib/samagotchi/session_inbox.rb +14 -8
- data/lib/samagotchi/session_manager.rb +11 -4
- data/lib/samagotchi/session_participants.rb +24 -3
- data/lib/samagotchi/verdict.rb +20 -0
- data/lib/samagotchi/verify_anchor.rb +28 -0
- data/lib/samagotchi/verify_fresh.rb +275 -0
- data/lib/samagotchi/verify_fresh_origin.rb +71 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +15 -0
- data/lib/samagotchi/web/participant_access.rb +31 -5
- data/lib/samagotchi/web/participant_review.rb +7 -1
- data/lib/samagotchi/web/participant_view.rb +46 -5
- data/lib/samagotchi/web/project_list.rb +52 -0
- data/lib/samagotchi/web/public/annotate_bar.js +9 -1
- data/lib/samagotchi/web/public/app.js +77 -4
- data/lib/samagotchi/web/public/composer.css +12 -1
- data/lib/samagotchi/web/public/data.js +2 -1
- data/lib/samagotchi/web/public/index.html +36 -16
- data/lib/samagotchi/web/public/notify.js +15 -2
- data/lib/samagotchi/web/public/participant.html +33 -32
- data/lib/samagotchi/web/public/participant.js +70 -7
- data/lib/samagotchi/web/public/project_picker.js +115 -0
- data/lib/samagotchi/web/public/restore.js +7 -4
- data/lib/samagotchi/web/public/scope.js +8 -10
- data/lib/samagotchi/web/public/send_turn.js +8 -1
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/stage_view.js +1 -1
- data/lib/samagotchi/web/public/stream_handlers.js +6 -1
- data/lib/samagotchi/web/public/verdict_card.js +65 -0
- data/lib/samagotchi/web/public/verdict_fresh.js +120 -0
- data/lib/samagotchi/web/public/verify_card.css +17 -0
- data/lib/samagotchi/web/push/attention.rb +48 -9
- data/lib/samagotchi/web/push/notifier.rb +4 -3
- data/lib/samagotchi/web/session_control.rb +2 -0
- data/lib/samagotchi/web/session_summary.rb +25 -1
- data/lib/samagotchi/web/verify_fresh_routes.rb +110 -0
- data/lib/samagotchi/worker.rb +75 -13
- data/lib/samagotchi/worker_inbound.rb +4 -4
- metadata +11 -1
|
@@ -86,11 +86,16 @@ module Samagotchi
|
|
|
86
86
|
state: state_of(session, row, reply), waiting: row[:waiting], live: !!row[:live],
|
|
87
87
|
delegate: session.delegate?, cwd: session.working_directory, branch: GitHead.branch(session.working_directory),
|
|
88
88
|
last_reply: reply && first_line(session.id, reply, state_dir: state_dir),
|
|
89
|
-
last_reply_at: reply && reply_time(reply), reported: !reply.nil? && cursor
|
|
89
|
+
last_reply_at: reply && reply_time(reply), reported: !reply.nil? && reported?(session, cursor, reply),
|
|
90
90
|
updated_at: session.updated_at, archived: !!row[:archived])
|
|
91
91
|
end
|
|
92
92
|
private_class_method :child
|
|
93
93
|
|
|
94
|
+
# A reply the parent was given; a Verify fresh child's always counts
|
|
95
|
+
# (its verdict goes to the parent's card, never a report).
|
|
96
|
+
def reported?(session, cursor, reply) = !session.verify_fresh.nil? || cursor.reply_file == reply
|
|
97
|
+
private_class_method :reported?
|
|
98
|
+
|
|
94
99
|
def state_of(session, row, reply)
|
|
95
100
|
return "waiting" if row[:waiting]
|
|
96
101
|
return "running" if Tools::Delegate.running?(row)
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -15,6 +15,8 @@ require_relative "steer"
|
|
|
15
15
|
require_relative "turn_note"
|
|
16
16
|
require_relative "turn_endings"
|
|
17
17
|
require_relative "turn_origin"
|
|
18
|
+
require_relative "verify_anchor"
|
|
19
|
+
require_relative "verdict"
|
|
18
20
|
require_relative "model_profile"
|
|
19
21
|
require_relative "effective_model"
|
|
20
22
|
require_relative "session_setting"
|
|
@@ -901,10 +903,12 @@ module Samagotchi
|
|
|
901
903
|
|
|
902
904
|
# Event types #announce accepts: facts about the session's input queue,
|
|
903
905
|
# a failed turn's prompt handed back and the continue offer, which live
|
|
904
|
-
# UIs need in the event log, emitted outside any turn's stream
|
|
906
|
+
# UIs need in the event log, emitted outside any turn's stream; a
|
|
907
|
+
# Verify fresh child's verdict landing on its card (:verdict_recorded).
|
|
905
908
|
ANNOUNCEABLE_EVENTS = %i[turn_enqueued turn_dequeued input_merged prompt_restored continue_offered continue_resolved
|
|
906
909
|
command_queued command_ran context_added card hook_notice guardrail_warning
|
|
907
|
-
plugin_init_started plugin_init_finished working_set_updated participants_changed
|
|
910
|
+
plugin_init_started plugin_init_finished working_set_updated participants_changed
|
|
911
|
+
verdict_recorded].freeze
|
|
908
912
|
|
|
909
913
|
# Put a transport-level event into the ordered event log. Unlike turn
|
|
910
914
|
# events it reaches only persistent observers (no turn sink, no memory
|
|
@@ -1589,7 +1593,10 @@ module Samagotchi
|
|
|
1589
1593
|
seconds = turn.elapsed
|
|
1590
1594
|
synchronize_events do
|
|
1591
1595
|
kept, event = yield(seconds)
|
|
1596
|
+
stamp_verify_anchor(kept, turn)
|
|
1592
1597
|
replace_session_messages(turn.session, kept) if kept
|
|
1598
|
+
verdict = record_verdict(turn, outcome)
|
|
1599
|
+
event[:verdict] = verdict if verdict
|
|
1593
1600
|
emit_working_set(turn.on_event, turn.session, turn.session.messages)
|
|
1594
1601
|
turn.session.status = Session::STATUS_IDLE
|
|
1595
1602
|
record_last_turn(turn.session, outcome, seconds, turn.origin, event, limit: exhausted ? turn.limit : nil)
|
|
@@ -1602,6 +1609,31 @@ module Samagotchi
|
|
|
1602
1609
|
end
|
|
1603
1610
|
private :end_turn
|
|
1604
1611
|
|
|
1612
|
+
def stamp_verify_anchor(messages, turn)
|
|
1613
|
+
return unless turn.verify && messages.is_a?(Array)
|
|
1614
|
+
|
|
1615
|
+
opening = messages.reverse.find do |message|
|
|
1616
|
+
(message[:role] || message["role"]).to_s == "user" &&
|
|
1617
|
+
(message[:turn_id] || message["turn_id"]).to_s == turn.id
|
|
1618
|
+
end
|
|
1619
|
+
opening[:verify] = turn.verify.to_h if opening
|
|
1620
|
+
end
|
|
1621
|
+
private :stamp_verify_anchor
|
|
1622
|
+
|
|
1623
|
+
def record_verdict(turn, outcome)
|
|
1624
|
+
return unless outcome == "completed" && turn.verify
|
|
1625
|
+
|
|
1626
|
+
answer = AnswerTail.find(turn.session.messages, turn_id: turn.id)
|
|
1627
|
+
parsed = Verdict.parse(answer && (answer[:content] || answer["content"]))
|
|
1628
|
+
return unless parsed
|
|
1629
|
+
|
|
1630
|
+
record = { anchor: turn.verify.to_h, verdict: parsed.verdict, reason: parsed.reason,
|
|
1631
|
+
by: turn.origin.author, check_turn_id: turn.id, at: Time.now.iso8601(3) }
|
|
1632
|
+
turn.session.verdicts = turn.session.verdicts + [record]
|
|
1633
|
+
record
|
|
1634
|
+
end
|
|
1635
|
+
private :record_verdict
|
|
1636
|
+
|
|
1605
1637
|
def save_quietly(session)
|
|
1606
1638
|
session.save(state_dir: session_state_dir)
|
|
1607
1639
|
rescue StandardError => e
|
|
@@ -2011,7 +2043,7 @@ module Samagotchi
|
|
|
2011
2043
|
# +wall+: the wall time it started (MessageTime.now, once): the
|
|
2012
2044
|
# :turn_started event's at and its user message's (and reminders') stamp.
|
|
2013
2045
|
Turn = Struct.new(:session, :prompt, :continue, :on_event, :controller, :origin, :started_at, :messages, :ended,
|
|
2014
|
-
:settings, :id, :limit, :resumed, :resume_from, :wall) do
|
|
2046
|
+
:settings, :id, :limit, :resumed, :resume_from, :wall, :verify) do
|
|
2015
2047
|
def resumed? = resumed ? true : false
|
|
2016
2048
|
|
|
2017
2049
|
# The turn's boundary events carry the origin only when it names a
|
|
@@ -2083,9 +2115,9 @@ module Samagotchi
|
|
|
2083
2115
|
# re-raises; a provider error adds error_kind:, retryable:, host: and a
|
|
2084
2116
|
# one-line summary:.
|
|
2085
2117
|
def run_turn(session, prompt, on_event: nil, max_iterations: nil, cancel_controller: nil, max_tool_output_chars: nil, pending_input: nil, continue: false,
|
|
2086
|
-
origin: TurnOrigin::NONE, images: [], id: nil, resumed: false, resume_from: nil)
|
|
2118
|
+
origin: TurnOrigin::NONE, images: [], id: nil, resumed: false, resume_from: nil, verify: nil)
|
|
2087
2119
|
turn = begin_turn(session, prompt, on_event: on_event, cancel_controller: cancel_controller, origin: origin,
|
|
2088
|
-
continue: continue, id: id)
|
|
2120
|
+
continue: continue, id: id, verify: verify)
|
|
2089
2121
|
if continue
|
|
2090
2122
|
turn.resumed = resumed
|
|
2091
2123
|
turn.resume_from = resume_from
|
|
@@ -2133,8 +2165,11 @@ module Samagotchi
|
|
|
2133
2165
|
# The turn's state before anything can fail it: the session running,
|
|
2134
2166
|
# the turn flag, its cancel controller and sink. The cross-thread state
|
|
2135
2167
|
# is set here and cleared in #release_turn only.
|
|
2136
|
-
def begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id: nil)
|
|
2168
|
+
def begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id: nil, verify: nil)
|
|
2137
2169
|
origin = TurnOrigin.of(origin)
|
|
2170
|
+
# A participant's Verify fresh child: every turn is theirs (their
|
|
2171
|
+
# author, boundary and cap), whoever started it.
|
|
2172
|
+
origin = session.verify_fresh.origin_for(origin) if session.respond_to?(:verify_fresh) && session.verify_fresh
|
|
2138
2173
|
# Track the active session for recap and status snapshot.
|
|
2139
2174
|
@session = session
|
|
2140
2175
|
# status is turn state: running now, idle again before the turn's end
|
|
@@ -2157,7 +2192,7 @@ module Samagotchi
|
|
|
2157
2192
|
|
|
2158
2193
|
Turn.new(session: session, prompt: continue ? nil : prompt, continue: continue, on_event: on_event,
|
|
2159
2194
|
controller: effective_controller, origin: origin, started_at: Process.clock_gettime(Process::CLOCK_MONOTONIC),
|
|
2160
|
-
id: id || SecureRandom.uuid, wall: MessageTime.now)
|
|
2195
|
+
id: id || SecureRandom.uuid, wall: MessageTime.now, verify: VerifyAnchor.from(verify))
|
|
2161
2196
|
end
|
|
2162
2197
|
private :begin_turn
|
|
2163
2198
|
|
|
@@ -2360,6 +2395,7 @@ module Samagotchi
|
|
|
2360
2395
|
unless turn.continue
|
|
2361
2396
|
user_message = { role: "user", content: turn.prompt, turn_id: turn.id, **author_fields(turn.origin.author),
|
|
2362
2397
|
**participant_mode_fields(turn.origin.author), MessageTime::KEY => turn.at }
|
|
2398
|
+
user_message[:verify] = turn.verify.to_h if turn.verify
|
|
2363
2399
|
user_message[:images] = image_refs unless image_refs.empty?
|
|
2364
2400
|
turn.messages << user_message
|
|
2365
2401
|
session.last_prompt = turn.prompt
|
|
@@ -109,9 +109,18 @@ module Samagotchi
|
|
|
109
109
|
@git = Guardrails::GitInfo.new
|
|
110
110
|
author = turn_author
|
|
111
111
|
@turn_entry = author && @participant.call(author)
|
|
112
|
-
@turn_mode = author &&
|
|
112
|
+
@turn_mode = author && participant_turn_mode(@session_lookup.call, @turn_entry)
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
# A participant's turn's mode: in a Verify fresh child the one saved
|
|
116
|
+
# on it at spawn (VerifyFreshOrigin#turn_mode: :review unless saved
|
|
117
|
+
# :pair), never looked up again; elsewhere ParticipantMode.for_turn.
|
|
118
|
+
def participant_turn_mode(session, entry)
|
|
119
|
+
fresh = session.respond_to?(:verify_fresh) && session.verify_fresh
|
|
120
|
+
fresh ? fresh.turn_mode : ParticipantMode.for_turn(session, entry)
|
|
121
|
+
end
|
|
122
|
+
private :participant_turn_mode
|
|
123
|
+
|
|
115
124
|
# The running participant turn's mode (ParticipantMode.for_turn: :pair
|
|
116
125
|
# only in a working session for a participant invited to act), decided
|
|
117
126
|
# at its start; nil for the owner's turns. The boundary in #checks, the
|
|
@@ -447,6 +447,7 @@ module Samagotchi
|
|
|
447
447
|
message[:images] = entry[:images] if entry[:images].is_a?(Array) && !entry[:images].empty?
|
|
448
448
|
message[:turn_id] = entry[:turn_id] if entry[:turn_id]
|
|
449
449
|
message[:turn_start] = true if entry[:turn_start] == true
|
|
450
|
+
message[:verify] = entry[:verify] if entry[:verify].is_a?(Hash)
|
|
450
451
|
message[:thinking] = entry[:thinking] if entry[:thinking].is_a?(String) && !entry[:thinking].empty?
|
|
451
452
|
message[:anthropic_content] = entry[:anthropic_content] if entry[:anthropic_content].is_a?(Array)
|
|
452
453
|
ToolResponse::SAVED_KEYS.each { |key| message[key] = entry[key] if entry[key] }
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -20,6 +20,10 @@ require_relative "project_scope"
|
|
|
20
20
|
require_relative "guardrails/parent_levels"
|
|
21
21
|
|
|
22
22
|
module Samagotchi
|
|
23
|
+
# Loaded on first use (a Verify fresh child): keeps the bundle test kit's
|
|
24
|
+
# load budget (spec/samagotchi/testing/sandbox_spec.rb).
|
|
25
|
+
autoload :VerifyFreshOrigin, File.expand_path("verify_fresh_origin", __dir__)
|
|
26
|
+
|
|
23
27
|
# archive_store requires this file.
|
|
24
28
|
autoload :ArchiveStore, File.expand_path("archive_store", __dir__)
|
|
25
29
|
|
|
@@ -134,6 +138,14 @@ module Samagotchi
|
|
|
134
138
|
@last_turn = LastTurn.from_file(value)
|
|
135
139
|
end
|
|
136
140
|
|
|
141
|
+
# Verdict cards recorded by completed Verify turns, in append order.
|
|
142
|
+
# A missing field stays missing until the first verdict is recorded.
|
|
143
|
+
def verdicts = @verdicts || []
|
|
144
|
+
|
|
145
|
+
def verdicts=(values)
|
|
146
|
+
@verdicts = values.nil? ? nil : Array(values)
|
|
147
|
+
end
|
|
148
|
+
|
|
137
149
|
# The mark of a turn a worker checkpointed and never finished
|
|
138
150
|
# (InterruptedTurn), as loaded; the next worker settles it and sets it
|
|
139
151
|
# to nil. While a turn runs, #turn_checkpoint's marker is saved instead.
|
|
@@ -280,6 +292,20 @@ module Samagotchi
|
|
|
280
292
|
# Whether this is a review session (chi review, the web's Start review).
|
|
281
293
|
def review? = !@review.nil?
|
|
282
294
|
|
|
295
|
+
# A Verify fresh child's requester, rules and claim (VerifyFreshOrigin),
|
|
296
|
+
# nil for any other session. Set once, at spawn (VerifyFresh.start).
|
|
297
|
+
attr_reader :verify_fresh
|
|
298
|
+
|
|
299
|
+
# @param value [VerifyFreshOrigin, Hash, nil] the session file's form too
|
|
300
|
+
def verify_fresh=(value)
|
|
301
|
+
@verify_fresh = value.nil? ? nil : VerifyFreshOrigin.of(value)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# The session whose participants.json names this session's
|
|
305
|
+
# participants: a Verify fresh child's parent (its requester's grant is
|
|
306
|
+
# the parent's), else the session itself.
|
|
307
|
+
def participants_source_id = @verify_fresh && @parent_id ? @parent_id : @id
|
|
308
|
+
|
|
283
309
|
# How many tool outputs its history holds: its tool messages
|
|
284
310
|
# (tool_response, a chat loop's tool; a native step's one message holds
|
|
285
311
|
# every call's output and counts once).
|
|
@@ -333,7 +359,7 @@ module Samagotchi
|
|
|
333
359
|
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil, prompt_notes: [],
|
|
334
360
|
continues: nil, thinking: nil, reasoning_history: nil, appended_prompt_files: [],
|
|
335
361
|
toolkit: nil, toolkit_origin: "chosen", settings: nil, interrupted_turn: nil, parent_answers: nil, title: nil,
|
|
336
|
-
working_set_pins: [], review: nil)
|
|
362
|
+
working_set_pins: [], review: nil, verdicts: nil, verify_fresh: nil)
|
|
337
363
|
@id = id
|
|
338
364
|
@metadata_version = metadata_version
|
|
339
365
|
@mode = mode
|
|
@@ -370,6 +396,8 @@ module Samagotchi
|
|
|
370
396
|
self.parent_answers = parent_answers
|
|
371
397
|
self.working_set_pins = working_set_pins
|
|
372
398
|
self.review = review
|
|
399
|
+
self.verdicts = verdicts
|
|
400
|
+
self.verify_fresh = verify_fresh
|
|
373
401
|
self.interrupted_turn = interrupted_turn
|
|
374
402
|
@turn_checkpoint = nil
|
|
375
403
|
@archived = false
|
|
@@ -539,6 +567,7 @@ module Samagotchi
|
|
|
539
567
|
"parent_id" => nil,
|
|
540
568
|
"scratch" => false,
|
|
541
569
|
"last_turn" => nil,
|
|
570
|
+
"verdicts" => nil,
|
|
542
571
|
"delegate" => false,
|
|
543
572
|
"llm_context" => nil,
|
|
544
573
|
"continues" => nil,
|
|
@@ -553,12 +582,13 @@ module Samagotchi
|
|
|
553
582
|
"parent_answers" => nil,
|
|
554
583
|
"working_set_pins" => [],
|
|
555
584
|
"review" => nil,
|
|
556
|
-
"first_turn_id" => nil
|
|
585
|
+
"first_turn_id" => nil,
|
|
586
|
+
"verify_fresh" => nil
|
|
557
587
|
}.freeze
|
|
558
588
|
|
|
559
589
|
# FIELDS a session file has only when set: a session without them saves
|
|
560
590
|
# as it did before they existed.
|
|
561
|
-
OPTIONAL_FIELDS = %w[review first_turn_id].freeze
|
|
591
|
+
OPTIONAL_FIELDS = %w[review first_turn_id verdicts verify_fresh].freeze
|
|
562
592
|
|
|
563
593
|
# A session from a parsed session file. +messages+ false leaves the
|
|
564
594
|
# conversation out (the lists' lightweight summaries).
|
|
@@ -712,6 +742,13 @@ module Samagotchi
|
|
|
712
742
|
when "last_turn" then @last_turn&.to_file
|
|
713
743
|
when "working_set_pins" then @working_set_pins.map(&:to_file)
|
|
714
744
|
when "review" then @review&.to_file
|
|
745
|
+
when "verify_fresh" then @verify_fresh&.to_file
|
|
746
|
+
when "verdicts"
|
|
747
|
+
@verdicts&.map do |record|
|
|
748
|
+
scrub_utf8(record.to_h do |key, entry|
|
|
749
|
+
[key.to_s, entry.is_a?(Hash) ? stringify_message_keys(entry) : entry]
|
|
750
|
+
end)
|
|
751
|
+
end
|
|
715
752
|
when "used_memory_names", "preloaded_memory_names", "muted_memory_names", "appended_prompt_files"
|
|
716
753
|
Array(instance_variable_get(:"@#{key}"))
|
|
717
754
|
else instance_variable_get(:"@#{key}")
|
|
@@ -12,6 +12,10 @@ require_relative "steer"
|
|
|
12
12
|
require_relative "turn_origin"
|
|
13
13
|
|
|
14
14
|
module Samagotchi
|
|
15
|
+
# Loaded on first use (a Verify turn's input): keeps the bundle test kit's
|
|
16
|
+
# load budget (spec/samagotchi/testing/sandbox_spec.rb).
|
|
17
|
+
autoload :VerifyAnchor, File.expand_path("verify_anchor", __dir__)
|
|
18
|
+
|
|
15
19
|
# A session directory's file inbox and outbox (see SessionManager for the
|
|
16
20
|
# layout): input/ holds one JSON file per queued turn, notes/ the context
|
|
17
21
|
# notes a worker adds between turns, output/ one text file per reply,
|
|
@@ -53,7 +57,7 @@ module Samagotchi
|
|
|
53
57
|
# @param now [Time] the time the file is named by (a spec's clock)
|
|
54
58
|
# @return [String, false] the input file's path, or false
|
|
55
59
|
def self.write_input(session_dir, prompt:, client_id: nil, enqueued_id: nil, no_interrupt: false, images: [],
|
|
56
|
-
delivery: nil, author: nil, now: Time.now)
|
|
60
|
+
delivery: nil, author: nil, verify: nil, now: Time.now)
|
|
57
61
|
images = Array(images)
|
|
58
62
|
input_dir = File.join(session_dir, INPUT_DIR)
|
|
59
63
|
FileUtils.mkdir_p(input_dir)
|
|
@@ -63,7 +67,7 @@ module Samagotchi
|
|
|
63
67
|
"no_interrupt" => (no_interrupt ? true : nil),
|
|
64
68
|
"delivery" => (Delivery.queue?(delivery) ? Delivery::QUEUE : nil),
|
|
65
69
|
"images" => (images.empty? ? nil : images.map { |image| image.transform_keys(&:to_s) }),
|
|
66
|
-
"author" => author }.compact
|
|
70
|
+
"author" => author, "verify" => VerifyAnchor.from(verify)&.to_file }.compact
|
|
67
71
|
AtomicFile.write(path, JSON.generate(record))
|
|
68
72
|
path
|
|
69
73
|
rescue StandardError
|
|
@@ -228,9 +232,9 @@ module Samagotchi
|
|
|
228
232
|
# (+no_interrupt+, --no-interrupt), its +images+ ({file:, name:} refs),
|
|
229
233
|
# and its +delivery+ ("queue" or nil; a file without the key is one an
|
|
230
234
|
# older chi wrote, so nil).
|
|
231
|
-
Input = Data.define(:prompt, :origin, :no_interrupt, :images, :delivery) do
|
|
232
|
-
def initialize(prompt:, origin: nil, no_interrupt: false, images: [], delivery: nil)
|
|
233
|
-
super(prompt:, origin: TurnOrigin.of(origin), no_interrupt:, images:, delivery:)
|
|
235
|
+
Input = Data.define(:prompt, :origin, :no_interrupt, :images, :delivery, :verify) do
|
|
236
|
+
def initialize(prompt:, origin: nil, no_interrupt: false, images: [], delivery: nil, verify: nil)
|
|
237
|
+
super(prompt:, origin: TurnOrigin.of(origin), no_interrupt:, images:, delivery:, verify: verify && VerifyAnchor.from(verify))
|
|
234
238
|
end
|
|
235
239
|
end
|
|
236
240
|
# A file that doesn't parse, or isn't a JSON object: no prompt.
|
|
@@ -246,7 +250,7 @@ module Samagotchi
|
|
|
246
250
|
origin = TurnOrigin.from(data.slice("client_id", "enqueued_id", "author"))
|
|
247
251
|
images = Array(data["images"]).select { |image| image.is_a?(Hash) }.map { |image| image.transform_keys(&:to_sym) }
|
|
248
252
|
Input.new(prompt: data["prompt"].to_s, origin: origin, no_interrupt: data["no_interrupt"] == true,
|
|
249
|
-
images: images, delivery: Delivery.queue?(data["delivery"]) ? Delivery::QUEUE : nil)
|
|
253
|
+
images: images, delivery: Delivery.queue?(data["delivery"]) ? Delivery::QUEUE : nil, verify: data["verify"])
|
|
250
254
|
rescue JSON::ParserError
|
|
251
255
|
UNREADABLE_INPUT
|
|
252
256
|
end
|
|
@@ -278,9 +282,11 @@ module Samagotchi
|
|
|
278
282
|
true
|
|
279
283
|
end
|
|
280
284
|
|
|
281
|
-
# A parsed input file's #waits_for_turn_end?.
|
|
285
|
+
# A parsed input file's #waits_for_turn_end?. A Verify turn waits too:
|
|
286
|
+
# merged into a running turn as a steer, its anchor would be lost.
|
|
282
287
|
def self.turn_end_data?(data)
|
|
283
|
-
Array(data["images"]).any? || Delivery.queue?(data["delivery"]) || Steer.participant_author?(data["author"])
|
|
288
|
+
Array(data["images"]).any? || Delivery.queue?(data["delivery"]) || Steer.participant_author?(data["author"]) ||
|
|
289
|
+
data["verify"].is_a?(Hash)
|
|
284
290
|
end
|
|
285
291
|
|
|
286
292
|
# How the unclaimed input file carrying +enqueued_id+ waits: :turn_end
|
|
@@ -164,6 +164,8 @@ module Samagotchi
|
|
|
164
164
|
# turn sees it
|
|
165
165
|
# @param review [SessionReview, nil] a review session's (ReviewStart; a
|
|
166
166
|
# continue keeps its previous link's)
|
|
167
|
+
# @param verify_fresh [VerifyFreshOrigin, nil] a Verify fresh child's
|
|
168
|
+
# requester, rules and claim (VerifyFresh.start)
|
|
167
169
|
# @param before_spawn [#call, nil] (session) runs once the session and
|
|
168
170
|
# its note are saved, before its worker spawns
|
|
169
171
|
# @return [Session] with #seed_images_dropped: refs whose file was gone,
|
|
@@ -171,7 +173,7 @@ module Samagotchi
|
|
|
171
173
|
def self.spawn_session(prompt:, mode: "assist", working_directory: nil, model_name: nil, state_dir: nil,
|
|
172
174
|
memories: [], muted_memories: [], parent_id: nil, messages: [], images_from: nil,
|
|
173
175
|
preview: nil, title: nil, delegate: false, setup: SessionSetup.new, continues: nil, note: nil,
|
|
174
|
-
note_source: "chi", before_spawn: nil, review: nil)
|
|
176
|
+
note_source: "chi", before_spawn: nil, review: nil, verify_fresh: nil)
|
|
175
177
|
sd = state_dir || Session.default_state_dir
|
|
176
178
|
# The resolved ref is stored (a resumed session keeps its model when an
|
|
177
179
|
# alias is retargeted), with the name as typed beside it. A wrong host
|
|
@@ -198,6 +200,8 @@ module Samagotchi
|
|
|
198
200
|
)
|
|
199
201
|
session.toolkit = Toolkit.configured(delegate: delegate) if session.toolkit.nil?
|
|
200
202
|
session.toolkit_origin = setup.toolkit_origin || "default"
|
|
203
|
+
# Saved before the worker starts: its first turn is already the requester's.
|
|
204
|
+
session.verify_fresh = verify_fresh
|
|
201
205
|
session.model_warning = model_warning
|
|
202
206
|
# With no prompt there is no first turn to run (an attaching UI sends
|
|
203
207
|
# the prompts), so the session starts idle.
|
|
@@ -1310,10 +1314,10 @@ module Samagotchi
|
|
|
1310
1314
|
# session's images/
|
|
1311
1315
|
# @return [String, false] the input file's path, or false.
|
|
1312
1316
|
def self.write_turn_input(session_id, prompt:, client_id: nil, enqueued_id: nil, no_interrupt: false, state_dir: nil,
|
|
1313
|
-
images: [], delivery: nil, author: nil)
|
|
1317
|
+
images: [], delivery: nil, author: nil, verify: nil)
|
|
1314
1318
|
session_dir = Session.session_dir(session_id, state_dir: state_dir || Session.default_state_dir)
|
|
1315
1319
|
SessionInbox.write_input(session_dir, prompt: prompt, client_id: client_id, enqueued_id: enqueued_id,
|
|
1316
|
-
no_interrupt: no_interrupt, images: images, delivery: delivery, author: author)
|
|
1320
|
+
no_interrupt: no_interrupt, images: images, delivery: delivery, author: author, verify: verify)
|
|
1317
1321
|
end
|
|
1318
1322
|
|
|
1319
1323
|
# How long a turn waits for the Bridge of a worker a resume just spawned.
|
|
@@ -1395,7 +1399,8 @@ module Samagotchi
|
|
|
1395
1399
|
# @raise [OwnedByTUI] a chi REPL owns the session
|
|
1396
1400
|
# @raise [ArgumentError] no such session
|
|
1397
1401
|
def self.deliver_turn(session_id, prompt:, client_id: nil, images: [], delivery: nil, no_interrupt: false,
|
|
1398
|
-
author: nil, pending_limit: nil, state_dir: nil, manager: self, bridge: nil)
|
|
1402
|
+
author: nil, pending_limit: nil, verify: nil, state_dir: nil, manager: self, bridge: nil)
|
|
1403
|
+
verify = VerifyAnchor.from(verify)
|
|
1399
1404
|
session_dir = Session.session_dir(session_id, state_dir: state_dir || Session.default_state_dir)
|
|
1400
1405
|
bridge ||= lambda do
|
|
1401
1406
|
link = WorkerLink.await(session_id, session_dir: session_dir, timeout: TURN_BRIDGE_WAIT)
|
|
@@ -1417,6 +1422,7 @@ module Samagotchi
|
|
|
1417
1422
|
options[:no_interrupt] = true if no_interrupt
|
|
1418
1423
|
options[:author] = author if author
|
|
1419
1424
|
options[:pending_limit] = pending_limit if author && pending_limit
|
|
1425
|
+
options[:verify] = verify if verify
|
|
1420
1426
|
reply = client.post_turn(**options)
|
|
1421
1427
|
ack = reply.json
|
|
1422
1428
|
return DeliverResult.new(status: :accepted, via: :bridge, ack: ack) if reply.status == 202 && ack.is_a?(Hash)
|
|
@@ -1454,6 +1460,7 @@ module Samagotchi
|
|
|
1454
1460
|
input[:delivery] = delivery unless Delivery.next_step?(delivery)
|
|
1455
1461
|
input[:no_interrupt] = true if no_interrupt
|
|
1456
1462
|
input[:author] = author if author
|
|
1463
|
+
input[:verify] = verify if verify
|
|
1457
1464
|
path = participant_queue_lock(session_dir, author && pending_limit) do
|
|
1458
1465
|
# No worker: the participant's waiting files are all their turns,
|
|
1459
1466
|
# counted and written under one lock (the Bridge counts under its).
|
|
@@ -22,8 +22,10 @@ module Samagotchi
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Reads +session+'s sidecar again.
|
|
25
|
+
# A Verify fresh child reads its parent's (Session#participants_source_id):
|
|
26
|
+
# its requester's grant is there.
|
|
25
27
|
def refresh!(session)
|
|
26
|
-
@session_id = session
|
|
28
|
+
@session_id = source_id(session)
|
|
27
29
|
@entries = read
|
|
28
30
|
self
|
|
29
31
|
end
|
|
@@ -44,7 +46,7 @@ module Samagotchi
|
|
|
44
46
|
def entry_for(author, session: nil)
|
|
45
47
|
return nil unless Steer.participant_author?(author)
|
|
46
48
|
|
|
47
|
-
refresh!(session) if session && session
|
|
49
|
+
refresh!(session) if session && source_id(session) != @session_id
|
|
48
50
|
id = Steer.participant_id(author)
|
|
49
51
|
@entries.find { |e| e.id == id } || (@entries = read).find { |e| e.id == id }
|
|
50
52
|
end
|
|
@@ -52,16 +54,35 @@ module Samagotchi
|
|
|
52
54
|
# The system prompt's participants line while anyone is in it (#active?
|
|
53
55
|
# as of the last #refresh!): ParticipantMode::PAIR when +session+ is a
|
|
54
56
|
# working session and an active participant was invited to act, else
|
|
55
|
-
# ParticipantMode::REVIEW; nil with nobody active.
|
|
57
|
+
# ParticipantMode::REVIEW; nil with nobody active. A Verify fresh
|
|
58
|
+
# child reads its parent's sidecar but has one participant at most, its
|
|
59
|
+
# requester: their saved mode while their grant is active, nil for the
|
|
60
|
+
# owner's child (no participant ever writes in it).
|
|
56
61
|
def prompt_mode(session)
|
|
57
62
|
active = @entries.select { |entry| entry.active?(@clock.call) }
|
|
58
63
|
return nil if active.empty?
|
|
59
64
|
|
|
65
|
+
fresh = session.respond_to?(:verify_fresh) && session.verify_fresh
|
|
66
|
+
return fresh_prompt_mode(fresh, active) if fresh
|
|
67
|
+
|
|
60
68
|
active.any? { |entry| ParticipantMode.for_turn(session, entry) == ParticipantMode::PAIR } ? ParticipantMode::PAIR : ParticipantMode::REVIEW
|
|
61
69
|
end
|
|
62
70
|
|
|
63
71
|
private
|
|
64
72
|
|
|
73
|
+
def fresh_prompt_mode(fresh, active)
|
|
74
|
+
return nil unless fresh.participant?
|
|
75
|
+
|
|
76
|
+
id = Steer.participant_id(fresh.author)
|
|
77
|
+
active.any? { |entry| entry.id == id } ? fresh.turn_mode : nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def source_id(session)
|
|
81
|
+
return nil unless session
|
|
82
|
+
|
|
83
|
+
session.respond_to?(:participants_source_id) ? session.participants_source_id : session.id
|
|
84
|
+
end
|
|
85
|
+
|
|
65
86
|
def read
|
|
66
87
|
return [] if @session_id.to_s.empty?
|
|
67
88
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# A parsed verdict line from a Verify turn's final answer.
|
|
5
|
+
Verdict = Data.define(:verdict, :reason) do
|
|
6
|
+
LINE = /\AVerdict:\s*(confirmed|refuted|unclear)\b\s*(?:—|–|-)\s*(.+?)\s*\z/i
|
|
7
|
+
MARKDOWN = /(?:\*\*|__|`)/
|
|
8
|
+
|
|
9
|
+
def self.parse(text)
|
|
10
|
+
line = text.to_s.lines.reverse_each.find { |candidate| !candidate.strip.empty? }
|
|
11
|
+
return unless line
|
|
12
|
+
|
|
13
|
+
normalized = line.strip.gsub(MARKDOWN, "")
|
|
14
|
+
match = LINE.match(normalized)
|
|
15
|
+
return unless match
|
|
16
|
+
|
|
17
|
+
new(verdict: match[1].downcase, reason: match[2].strip)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# A claim selected in an earlier turn, carried only by a Verify turn.
|
|
5
|
+
VerifyAnchor = Data.define(:turn_id, :quote) do
|
|
6
|
+
UUID = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
7
|
+
QUOTE_MAX = 200
|
|
8
|
+
|
|
9
|
+
def self.from(value)
|
|
10
|
+
return value if value.is_a?(self)
|
|
11
|
+
return nil unless value.is_a?(Hash)
|
|
12
|
+
|
|
13
|
+
fields = value.to_h { |key, entry| [key.to_s, entry] }
|
|
14
|
+
return nil unless fields.keys.sort == %w[quote turn_id]
|
|
15
|
+
|
|
16
|
+
turn_id = fields["turn_id"]
|
|
17
|
+
quote = fields["quote"]
|
|
18
|
+
return nil unless turn_id.is_a?(String) && UUID.match?(turn_id)
|
|
19
|
+
return nil unless quote.is_a?(String) && quote.length.between?(1, QUOTE_MAX) && !quote.strip.empty?
|
|
20
|
+
|
|
21
|
+
new(turn_id:, quote:)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h = { turn_id:, quote: }
|
|
25
|
+
|
|
26
|
+
def to_file = { "turn_id" => turn_id, "quote" => quote }
|
|
27
|
+
end
|
|
28
|
+
end
|