samagotchi 0.107.0 → 0.108.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 +37 -1
- data/docs/architecture.md +3 -2
- data/docs/internals/engine.md +17 -6
- data/docs/internals/participants.md +5 -2
- data/docs/internals/working-set.md +2 -1
- data/docs/memory.md +20 -0
- data/docs/plugins.md +9 -2
- data/docs/releasing.md +11 -6
- data/docs/sessions.md +3 -3
- data/docs/sub-agent.md +1 -1
- data/docs/testing.md +5 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -2
- data/lib/samagotchi/bridge.rb +3 -2
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +2 -2
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +51 -9
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/work-cap/manifest.yml +2 -2
- data/lib/samagotchi/bundles/work-cap/plugin.rb +50 -14
- data/lib/samagotchi/child_reports.rb +2 -1
- data/lib/samagotchi/cli/parent_wait.rb +1 -1
- data/lib/samagotchi/context_sources.rb +1 -6
- data/lib/samagotchi/continue_offer.rb +8 -6
- data/lib/samagotchi/engine.rb +36 -25
- data/lib/samagotchi/guardrail_wiring.rb +9 -6
- data/lib/samagotchi/idle_scheduler.rb +17 -2
- data/lib/samagotchi/last_turn.rb +7 -0
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/memory_orphans.rb +74 -0
- data/lib/samagotchi/memory_paths.rb +4 -4
- data/lib/samagotchi/parent_report.rb +11 -3
- data/lib/samagotchi/reply_wait.rb +7 -2
- data/lib/samagotchi/send_command/busy.rb +5 -2
- data/lib/samagotchi/send_command/wait_target.rb +16 -11
- data/lib/samagotchi/session_inbox.rb +12 -8
- data/lib/samagotchi/session_metrics.rb +7 -6
- data/lib/samagotchi/session_participants.rb +3 -1
- data/lib/samagotchi/steer.rb +11 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +30 -26
- data/lib/samagotchi/terminal_ui/formatting.rb +2 -2
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +5 -1
- data/lib/samagotchi/tools/memory.rb +16 -9
- data/lib/samagotchi/turn_origin.rb +68 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +2 -0
- data/lib/samagotchi/web/grants.rb +2 -1
- data/lib/samagotchi/web/participant_access.rb +6 -116
- data/lib/samagotchi/web/participant_view.rb +7 -13
- data/lib/samagotchi/web/participants_admin.rb +129 -0
- data/lib/samagotchi/web/public/participant.js +2 -2
- data/lib/samagotchi/web/public/participant_model.js +5 -0
- data/lib/samagotchi/web/public/participant_names.js +2 -2
- data/lib/samagotchi/web/public/participants_panel.js +2 -2
- data/lib/samagotchi/worker.rb +30 -25
- data/lib/samagotchi/worker_inbound.rb +12 -9
- data/lib/samagotchi/working_set.rb +3 -2
- metadata +4 -1
|
@@ -221,10 +221,15 @@ module Samagotchi
|
|
|
221
221
|
# merged): that turn runs (its mark has another id, or none from a
|
|
222
222
|
# worker that writes ids), or, idle, a turn not its ended since the
|
|
223
223
|
# baseline. last_turn or a reply file being its own is never that, nor
|
|
224
|
-
# an input with a tombstone (a removal's, written before its claim)
|
|
224
|
+
# an input with a tombstone (a removal's, written before its claim),
|
|
225
|
+
# nor one whose claimed file is still there: a drain deletes the file
|
|
226
|
+
# it merges as it reads it, while a turn of its own keeps it until the
|
|
227
|
+
# turn ends (WorkerInbound#take_input), so a look between that claim
|
|
228
|
+
# and the turn's running save (idle, an earlier turn's end since the
|
|
229
|
+
# baseline) doesn't take the earlier turn's reply for it.
|
|
225
230
|
def merged_elsewhere?(session, turn, baseline, session_dir)
|
|
226
231
|
return false if session.last_turn&.enqueued_id == turn || SessionInbox.reply_for(session_dir, turn)
|
|
227
|
-
return false if SessionInbox.removed?(session_dir, turn)
|
|
232
|
+
return false if SessionInbox.removed?(session_dir, turn) || SessionInbox.claimed_input?(session_dir, turn)
|
|
228
233
|
|
|
229
234
|
if session.status == Session::STATUS_RUNNING
|
|
230
235
|
running_id = session.interrupted_turn&.enqueued_id
|
|
@@ -40,9 +40,12 @@ module Samagotchi
|
|
|
40
40
|
# What the user is told, after the session's short id.
|
|
41
41
|
# @param via [ChiInvocation] how its commands are spelled
|
|
42
42
|
def message(via: ChiInvocation::HERE)
|
|
43
|
-
return "busy: a turn is running; wait or #{via.remote? ? "open it in #{via.web}" : "attach"}" unless error == "waiting"
|
|
44
|
-
|
|
45
43
|
id = session_id
|
|
44
|
+
unless error == "waiting"
|
|
45
|
+
return "busy: a turn is running; --queue runs the message after it, " \
|
|
46
|
+
"#{via.command("send --wait #{id}")} waits for it, #{via.attach(id)} shows it"
|
|
47
|
+
end
|
|
48
|
+
|
|
46
49
|
# The whole question (its options, its tags) for what a parent may
|
|
47
50
|
# give.
|
|
48
51
|
q = ParentReport.question(question, strategy: strategy)
|
|
@@ -13,30 +13,35 @@ module Samagotchi
|
|
|
13
13
|
WaitTarget = Data.define(:turn, :report_turn_id, :queued, :ended_before) do
|
|
14
14
|
def initialize(turn: nil, report_turn_id: nil, queued: false, ended_before: false) = super
|
|
15
15
|
|
|
16
|
-
# A --new session's first message
|
|
17
|
-
#
|
|
18
|
-
# turn)
|
|
16
|
+
# A --new session's first message: its own turn, the ack's id (a
|
|
17
|
+
# file-delivered one's also names the outcome of a session command
|
|
18
|
+
# that runs no turn), so a worker that dies in it and the next one's
|
|
19
|
+
# settle end the wait (TurnCheckpoint.settled_turn). A message merged
|
|
20
|
+
# into a turn starting then falls back to that turn's reply
|
|
21
|
+
# (ReplyWait's turn: without queued:); no id (an older worker), the
|
|
22
|
+
# next reply.
|
|
19
23
|
# @param delivered [SessionManager::DeliverResult, nil] nil when the
|
|
20
24
|
# message started the session itself (no delivery)
|
|
21
25
|
def self.first_message(delivered)
|
|
22
26
|
return new unless delivered
|
|
23
27
|
|
|
24
|
-
new(turn: delivered.
|
|
28
|
+
new(turn: delivered.enqueued_id, report_turn_id: delivered.enqueued_id)
|
|
25
29
|
end
|
|
26
30
|
|
|
27
31
|
# A message to an existing session. --queue: its own turn, the ack's
|
|
28
32
|
# id, also when the turn running at the check ended before it went in
|
|
29
33
|
# (an idle worker's ack says next_step, and the file still runs as
|
|
30
|
-
# that turn). A file fallback's id names its turn too
|
|
31
|
-
#
|
|
32
|
-
# (
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
34
|
+
# that turn). A file fallback's id names its turn too, and so does a
|
|
35
|
+
# plain message to an idle session, as #first_message's. Otherwise
|
|
36
|
+
# (a session left running by a dead worker, the busy check's owner
|
|
37
|
+
# gone) the next reply, reported as the turn that was running when it
|
|
38
|
+
# went in (+running_turn+, ReplyWait.own_turn). A plain message the
|
|
39
|
+
# worker queued (its ack says queue: a participant's review turn was
|
|
40
|
+
# running, which takes no steer) is a --queue's.
|
|
36
41
|
# @param running [Boolean] the session was running when it went in
|
|
37
42
|
def self.message(delivered, queue:, running:, running_turn:)
|
|
38
43
|
queue ||= delivered.delivery == Delivery::QUEUE
|
|
39
|
-
turn = delivered.enqueued_id if queue || delivered.file?
|
|
44
|
+
turn = delivered.enqueued_id if queue || delivered.file? || !running
|
|
40
45
|
report = if queue then turn
|
|
41
46
|
elsif running then running_turn
|
|
42
47
|
else delivered.enqueued_id
|
|
@@ -9,6 +9,7 @@ require_relative "delivery"
|
|
|
9
9
|
require_relative "log"
|
|
10
10
|
require_relative "session"
|
|
11
11
|
require_relative "steer"
|
|
12
|
+
require_relative "turn_origin"
|
|
12
13
|
|
|
13
14
|
module Samagotchi
|
|
14
15
|
# A session directory's file inbox and outbox (see SessionManager for the
|
|
@@ -221,13 +222,16 @@ module Samagotchi
|
|
|
221
222
|
Dir.glob(File.join(input_dir, "*.json"))
|
|
222
223
|
end
|
|
223
224
|
|
|
224
|
-
# A claimed input file as read: its +prompt+, +origin+ (
|
|
225
|
-
#
|
|
226
|
-
# with the raised iteration limit
|
|
227
|
-
# +images+ ({file:, name:} refs),
|
|
228
|
-
# file without the key is one an
|
|
225
|
+
# A claimed input file as read: its +prompt+, +origin+ (a TurnOrigin
|
|
226
|
+
# from the file's client_id, enqueued_id and author; nil when it names
|
|
227
|
+
# no sender), whether its turn runs with the raised iteration limit
|
|
228
|
+
# (+no_interrupt+, --no-interrupt), its +images+ ({file:, name:} refs),
|
|
229
|
+
# and its +delivery+ ("queue" or nil; a file without the key is one an
|
|
230
|
+
# older chi wrote, so nil).
|
|
229
231
|
Input = Data.define(:prompt, :origin, :no_interrupt, :images, :delivery) do
|
|
230
|
-
def initialize(prompt:, origin: nil, no_interrupt: false, images: [], delivery: nil)
|
|
232
|
+
def initialize(prompt:, origin: nil, no_interrupt: false, images: [], delivery: nil)
|
|
233
|
+
super(prompt:, origin: TurnOrigin.of(origin), no_interrupt:, images:, delivery:)
|
|
234
|
+
end
|
|
231
235
|
end
|
|
232
236
|
# A file that doesn't parse, or isn't a JSON object: no prompt.
|
|
233
237
|
UNREADABLE_INPUT = Input.new(prompt: nil)
|
|
@@ -239,9 +243,9 @@ module Samagotchi
|
|
|
239
243
|
return UNREADABLE_INPUT unless data.is_a?(Hash)
|
|
240
244
|
|
|
241
245
|
# author: only a file that names one (an owner's names none).
|
|
242
|
-
origin =
|
|
246
|
+
origin = TurnOrigin.from(data.slice("client_id", "enqueued_id", "author"))
|
|
243
247
|
images = Array(data["images"]).select { |image| image.is_a?(Hash) }.map { |image| image.transform_keys(&:to_sym) }
|
|
244
|
-
Input.new(prompt: data["prompt"].to_s, origin: origin
|
|
248
|
+
Input.new(prompt: data["prompt"].to_s, origin: origin, no_interrupt: data["no_interrupt"] == true,
|
|
245
249
|
images: images, delivery: Delivery.queue?(data["delivery"]) ? Delivery::QUEUE : nil)
|
|
246
250
|
rescue JSON::ParserError
|
|
247
251
|
UNREADABLE_INPUT
|
|
@@ -19,6 +19,7 @@ require_relative "session_metrics/usage"
|
|
|
19
19
|
require_relative "session_metrics/side_totals"
|
|
20
20
|
require_relative "side_usage"
|
|
21
21
|
require_relative "steer"
|
|
22
|
+
require_relative "turn_origin"
|
|
22
23
|
|
|
23
24
|
module Samagotchi
|
|
24
25
|
# SessionMetrics is a persistent, error-isolated collector of per-session
|
|
@@ -727,6 +728,7 @@ module Samagotchi
|
|
|
727
728
|
load_persisted
|
|
728
729
|
end
|
|
729
730
|
@started_at ||= now.iso8601(3)
|
|
731
|
+
origin = TurnOrigin.from(event[:origin])
|
|
730
732
|
@turn = TurnState.new(
|
|
731
733
|
session_id: @session_id,
|
|
732
734
|
iteration_count: 0,
|
|
@@ -766,8 +768,8 @@ module Samagotchi
|
|
|
766
768
|
gen_switch: false,
|
|
767
769
|
continue: event[:continue] ? true : false,
|
|
768
770
|
resumed: event[:resumed] ? true : false,
|
|
769
|
-
author:
|
|
770
|
-
automatic: automatic_origin?(
|
|
771
|
+
author: origin&.participant_author,
|
|
772
|
+
automatic: automatic_origin?(origin),
|
|
771
773
|
id: event[:turn_id] || SecureRandom.uuid,
|
|
772
774
|
started_at: now.iso8601(3),
|
|
773
775
|
started_monotonic: monotonic_time,
|
|
@@ -818,11 +820,10 @@ module Samagotchi
|
|
|
818
820
|
end
|
|
819
821
|
end
|
|
820
822
|
|
|
821
|
-
# Whether a turn's +origin+ names a client that is no
|
|
822
|
-
# without one (an in-process turn is the owner's).
|
|
823
|
+
# Whether a turn's +origin+ (a TurnOrigin) names a client that is no
|
|
824
|
+
# person's: false without one (an in-process turn is the owner's).
|
|
823
825
|
def automatic_origin?(origin)
|
|
824
|
-
client_id
|
|
825
|
-
!client_id.nil? && !ClientId.human?(client_id)
|
|
826
|
+
!origin&.client_id.nil? && !origin.human?
|
|
826
827
|
end
|
|
827
828
|
|
|
828
829
|
# The running turn's record so far, as end_turn would make it for a
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "steer"
|
|
4
|
+
|
|
3
5
|
module Samagotchi
|
|
4
6
|
# Loaded only when a session is read: it brings Web::Grants (and Rack).
|
|
5
7
|
autoload :Participants, File.expand_path("participants", __dir__)
|
|
@@ -38,7 +40,7 @@ module Samagotchi
|
|
|
38
40
|
return nil unless Steer.participant_author?(author)
|
|
39
41
|
|
|
40
42
|
refresh!(session) if session && session.id != @session_id
|
|
41
|
-
id =
|
|
43
|
+
id = Steer.participant_id(author)
|
|
42
44
|
entry = @entries.find { |e| e.id == id } || (@entries = read).find { |e| e.id == id }
|
|
43
45
|
entry&.name
|
|
44
46
|
end
|
data/lib/samagotchi/steer.rb
CHANGED
|
@@ -128,6 +128,9 @@ module Samagotchi
|
|
|
128
128
|
PARTICIPANT_SOURCE = "participant"
|
|
129
129
|
# The owner's author, when one is written out; absent means the same.
|
|
130
130
|
OWNER_AUTHOR = "owner"
|
|
131
|
+
# A participant's author: this prefix, then its id (Web::Grants'
|
|
132
|
+
# grant id), "p:<id>" (#author_for, #participant_id).
|
|
133
|
+
PARTICIPANT_PREFIX = "p:"
|
|
131
134
|
# A participant's message's keys, which every message copy keeps.
|
|
132
135
|
AUTHOR_KEYS = %i[author author_name].freeze
|
|
133
136
|
|
|
@@ -187,7 +190,7 @@ module Samagotchi
|
|
|
187
190
|
# again (ParticipantName), or the participant's id when it has none.
|
|
188
191
|
def review_header(message)
|
|
189
192
|
name = ParticipantName.clean(message[:author_name] || message["author_name"])
|
|
190
|
-
name = ParticipantName.clean((message[:author] || message["author"])
|
|
193
|
+
name = ParticipantName.clean(participant_id(message[:author] || message["author"])) if name.empty?
|
|
191
194
|
format(REVIEW_HEADER, name.empty? ? "a participant" : name)
|
|
192
195
|
end
|
|
193
196
|
|
|
@@ -219,6 +222,13 @@ module Samagotchi
|
|
|
219
222
|
# +author+ when it is a participant's, else nil (the owner).
|
|
220
223
|
def sender_author(author) = participant_author?(author) ? author.to_s : nil
|
|
221
224
|
|
|
225
|
+
# The author a participant's turns and messages carry: "p:<id>".
|
|
226
|
+
def author_for(participant_id) = "#{PARTICIPANT_PREFIX}#{participant_id}"
|
|
227
|
+
|
|
228
|
+
# The participant id in +author+ ("p:<id>" → "<id>"); a string without
|
|
229
|
+
# the prefix as it is ("" for nil).
|
|
230
|
+
def participant_id(author) = author.to_s.delete_prefix(PARTICIPANT_PREFIX)
|
|
231
|
+
|
|
222
232
|
# The text a steer or merged input goes out as: its header, a newline,
|
|
223
233
|
# then +text+ (the message's content by default). Other messages: +text+.
|
|
224
234
|
def wire_text(message, text = (message[:content] || message["content"]).to_s)
|
|
@@ -22,6 +22,7 @@ require_relative "../delivery"
|
|
|
22
22
|
require_relative "../log"
|
|
23
23
|
require_relative "../context_note"
|
|
24
24
|
require_relative "../steer"
|
|
25
|
+
require_relative "../turn_origin"
|
|
25
26
|
require_relative "../model_profile"
|
|
26
27
|
require_relative "../output_formatter"
|
|
27
28
|
require_relative "../session_commands"
|
|
@@ -317,7 +318,7 @@ module Samagotchi
|
|
|
317
318
|
when :input_merged
|
|
318
319
|
merged_own_prompts(event)
|
|
319
320
|
# A delegate's report: a child's state changed.
|
|
320
|
-
refresh_children if Array(event[:origins]).any? { |origin| report_origin?(origin) }
|
|
321
|
+
refresh_children if Array(event[:origins]).any? { |origin| report_origin?(origin_of(origin)) }
|
|
321
322
|
when :question_requested
|
|
322
323
|
# Nothing is left to answer it with.
|
|
323
324
|
return unanswered_question(event[:pending_question]) if @input_ended
|
|
@@ -973,7 +974,7 @@ module Samagotchi
|
|
|
973
974
|
def own_turn_ended(event)
|
|
974
975
|
return unless @wait_at_eof
|
|
975
976
|
|
|
976
|
-
id = (event[:origin]
|
|
977
|
+
id = origin_of(event[:origin]).enqueued_id
|
|
977
978
|
ours = @open_ids.include?(id) || !@merged_ids.empty?
|
|
978
979
|
@own_failed = true if ours && event[:type] == :turn_failed
|
|
979
980
|
@own_empty = true if ours && event[:type] == :turn_completed && event.dig(:turn_summary, :empty_answer)
|
|
@@ -986,9 +987,10 @@ module Samagotchi
|
|
|
986
987
|
|
|
987
988
|
# Ours merged into the running turn: they end with it.
|
|
988
989
|
def merged_own_prompts(event)
|
|
989
|
-
Array(event[:origins]).each do |
|
|
990
|
-
|
|
991
|
-
|
|
990
|
+
Array(event[:origins]).each do |value|
|
|
991
|
+
origin = origin_of(value)
|
|
992
|
+
id = origin.enqueued_id
|
|
993
|
+
@merged_ids << id if own?(origin.client_id) && @open_ids.include?(id)
|
|
992
994
|
end
|
|
993
995
|
nil
|
|
994
996
|
end
|
|
@@ -1188,8 +1190,8 @@ module Samagotchi
|
|
|
1188
1190
|
# The worker rolled a failed turn back and handed its prompt back: ours
|
|
1189
1191
|
# goes back into the input, as the REPL restores it for a retry.
|
|
1190
1192
|
def restore_prompt(event)
|
|
1191
|
-
origin = event[:origin]
|
|
1192
|
-
return unless own?(origin
|
|
1193
|
+
origin = origin_of(event[:origin])
|
|
1194
|
+
return unless own?(origin.client_id) && @sent_ids.include?(origin.enqueued_id)
|
|
1193
1195
|
# Lines from a pipe: nobody to retry it (the REPL's rule).
|
|
1194
1196
|
return if @wait_at_eof
|
|
1195
1197
|
|
|
@@ -1342,7 +1344,7 @@ module Samagotchi
|
|
|
1342
1344
|
next if own?(entry[:client_id])
|
|
1343
1345
|
|
|
1344
1346
|
@shown_enqueued << entry[:enqueued_id]
|
|
1345
|
-
@screen.commit("queued #{prompt_line(entry[:client_id], entry[:prompt], **author_of(entry))}")
|
|
1347
|
+
@screen.commit("queued #{prompt_line(entry[:client_id], entry[:prompt], **author_of(entry[:author]))}")
|
|
1346
1348
|
end
|
|
1347
1349
|
# Commands waiting for the turn's end (their command_ran prints no
|
|
1348
1350
|
# line); a resync prints only the ones it hadn't shown.
|
|
@@ -1606,9 +1608,9 @@ module Samagotchi
|
|
|
1606
1608
|
return unless turn
|
|
1607
1609
|
|
|
1608
1610
|
continues = turn[:prompt].nil?
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
@screen.commit(prompt_line(origin
|
|
1611
|
+
origin = origin_of(turn[:origin])
|
|
1612
|
+
unless continues && reminder_origin?(origin)
|
|
1613
|
+
@screen.commit(prompt_line(origin.client_id, turn[:prompt] || continuing_text(origin), **author_of(origin.author)))
|
|
1612
1614
|
end
|
|
1613
1615
|
tail = nil
|
|
1614
1616
|
lane = :writing
|
|
@@ -1657,7 +1659,7 @@ module Samagotchi
|
|
|
1657
1659
|
return if own?(event[:client_id])
|
|
1658
1660
|
|
|
1659
1661
|
@shown_enqueued << event[:enqueued_id]
|
|
1660
|
-
line = prompt_line(event[:client_id], event[:prompt], **author_of(event))
|
|
1662
|
+
line = prompt_line(event[:client_id], event[:prompt], **author_of(event[:author]))
|
|
1661
1663
|
@screen.commit(event[:delivery] == Delivery::QUEUE ? "#{line} #{QUEUED_NOTE}" : line)
|
|
1662
1664
|
end
|
|
1663
1665
|
|
|
@@ -1684,18 +1686,18 @@ module Samagotchi
|
|
|
1684
1686
|
def start_turn(event)
|
|
1685
1687
|
@running = true
|
|
1686
1688
|
@joined_mid_turn = false
|
|
1687
|
-
origin = event[:origin]
|
|
1688
|
-
@my_queued.delete(origin
|
|
1689
|
+
origin = origin_of(event[:origin])
|
|
1690
|
+
@my_queued.delete(origin.enqueued_id)
|
|
1689
1691
|
if event[:prompt].nil?
|
|
1690
1692
|
# A continue turn (after the offer's yes) has no prompt to show; a
|
|
1691
1693
|
# reminder turn shows its reminders (:reminder_injected).
|
|
1692
1694
|
# A turn run for delegate reports shows them as they merge; one
|
|
1693
1695
|
# an attached context change started, its label (the note is above).
|
|
1694
1696
|
unless reminder_origin?(origin) || report_origin?(origin)
|
|
1695
|
-
@screen.commit(prompt_line(origin
|
|
1697
|
+
@screen.commit(prompt_line(origin.client_id, continuing_text(origin), **author_of(origin.author)))
|
|
1696
1698
|
end
|
|
1697
|
-
elsif !(own?(origin
|
|
1698
|
-
@screen.commit(prompt_line(origin
|
|
1699
|
+
elsif !(own?(origin.client_id) || @shown_enqueued.include?(origin.enqueued_id))
|
|
1700
|
+
@screen.commit(prompt_line(origin.client_id, event[:prompt], **author_of(origin.author)))
|
|
1699
1701
|
end
|
|
1700
1702
|
# The own-send echo already names an image-only prompt; another
|
|
1701
1703
|
# client's prompt line does too, here or at its enqueue.
|
|
@@ -1755,14 +1757,14 @@ module Samagotchi
|
|
|
1755
1757
|
end
|
|
1756
1758
|
|
|
1757
1759
|
def learn_participants(rows)
|
|
1758
|
-
Array(rows).each { |row| @participant_names[
|
|
1760
|
+
Array(rows).each { |row| @participant_names[Steer.author_for(row[:id])] = row[:name] if row.is_a?(Hash) && row[:id] }
|
|
1759
1761
|
end
|
|
1760
1762
|
|
|
1761
|
-
# The prompt line's author keywords for an event, an
|
|
1762
|
-
# queued row (
|
|
1763
|
+
# The prompt line's author keywords for the +author+ of an event, an
|
|
1764
|
+
# origin or a queued row ("p:<id>"): their name as the worker named
|
|
1763
1765
|
# them; {} for the owner's.
|
|
1764
|
-
def author_of(
|
|
1765
|
-
author = Steer.sender_author(
|
|
1766
|
+
def author_of(author)
|
|
1767
|
+
author = Steer.sender_author(author) or return {}
|
|
1766
1768
|
|
|
1767
1769
|
{ author: author, author_name: @participant_names[author] }
|
|
1768
1770
|
end
|
|
@@ -1784,10 +1786,12 @@ module Samagotchi
|
|
|
1784
1786
|
@default_model_name ||= @link.default_model_name
|
|
1785
1787
|
end
|
|
1786
1788
|
|
|
1787
|
-
|
|
1788
|
-
def
|
|
1789
|
-
def
|
|
1790
|
-
def
|
|
1789
|
+
# An event's origin (its wire Hash) as a TurnOrigin, NONE for none.
|
|
1790
|
+
def origin_of(value) = TurnOrigin.of(value)
|
|
1791
|
+
def reminder_origin?(origin) = origin.client_id.to_s.start_with?(ClientId::SYSTEM_PREFIX)
|
|
1792
|
+
def report_origin?(origin) = origin.source == "delegate_report"
|
|
1793
|
+
def context_origin?(origin) = origin.source == "context"
|
|
1794
|
+
def task_origin?(origin) = origin.source == "task"
|
|
1791
1795
|
# A turn with no prompt: "(continuing)"; a context wake's or a task
|
|
1792
1796
|
# finish wake's label alone.
|
|
1793
1797
|
def continuing_text(origin) = context_origin?(origin) || task_origin?(origin) ? nil : "(continuing)"
|
|
@@ -251,7 +251,7 @@ module Samagotchi
|
|
|
251
251
|
def participant_prefix(author, name)
|
|
252
252
|
return "" unless Steer.participant_author?(author)
|
|
253
253
|
|
|
254
|
-
id =
|
|
254
|
+
id = Steer.participant_id(author)
|
|
255
255
|
"#{paint("#{participant_display_name(id, name)} ▸", participant_colour(id))} "
|
|
256
256
|
end
|
|
257
257
|
|
|
@@ -632,7 +632,7 @@ module Samagotchi
|
|
|
632
632
|
return nil unless rows.is_a?(Hash) && !rows.empty?
|
|
633
633
|
|
|
634
634
|
rows.map do |author, row|
|
|
635
|
-
name = row[:name].to_s.empty? ?
|
|
635
|
+
name = row[:name].to_s.empty? ? Steer.participant_id(author) : row[:name]
|
|
636
636
|
turns = row[:turns].to_i
|
|
637
637
|
format("%<name>s $%<cost>.2f (%<turns>d turn%<s>s)", name: name, cost: row[:cost].to_f, turns: turns,
|
|
638
638
|
s: turns == 1 ? "" : "s")
|
|
@@ -15,7 +15,10 @@ module Samagotchi
|
|
|
15
15
|
# a failed request (:generation_retrying). And the hooks' notices
|
|
16
16
|
# (:hook_notice): one during the turn as it comes, one the after_turn
|
|
17
17
|
# hooks give (after :turn_completed; source-links' `sources:`) kept for
|
|
18
|
-
# #flush, which the caller runs after it has printed the answer.
|
|
18
|
+
# #flush, which the caller runs after it has printed the answer. And
|
|
19
|
+
# the load warnings the first turn announces (:guardrail_warning: the
|
|
20
|
+
# guardrails, the plugins, an orphaned project memory folder), as the
|
|
21
|
+
# REPL's "<label>> …" lines.
|
|
19
22
|
class OneShotSink
|
|
20
23
|
include Formatting
|
|
21
24
|
|
|
@@ -33,6 +36,7 @@ module Samagotchi
|
|
|
33
36
|
when :llm_context_edited then line(format_llm_context_line(event))
|
|
34
37
|
when :generation_retrying then line(format_generation_retry_line(event))
|
|
35
38
|
when :turn_completed, :turn_canceled then @turn_over = true
|
|
39
|
+
when :guardrail_warning then line(EventRenderer.load_warning_line(event))
|
|
36
40
|
when :hook_notice
|
|
37
41
|
text = EventRenderer.hook_notice_line(event)
|
|
38
42
|
@turn_over ? @held << text : line(text)
|
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "digest"
|
|
5
5
|
require "date"
|
|
6
6
|
require_relative "../atomic_file"
|
|
7
|
+
require_relative "../memory_orphans"
|
|
7
8
|
require_relative "../memory_paths"
|
|
8
9
|
require_relative "../model_match"
|
|
9
10
|
|
|
@@ -30,9 +31,9 @@ module Samagotchi
|
|
|
30
31
|
scope = normalize_scope(scope)
|
|
31
32
|
|
|
32
33
|
if entry_name.empty?
|
|
33
|
-
return scoped_index(scope) if scope
|
|
34
|
+
return scoped_index(scope) + (scope == "project" ? orphan_hint : "") if scope
|
|
34
35
|
|
|
35
|
-
project = scoped_index("project")
|
|
36
|
+
project = scoped_index("project") + orphan_hint
|
|
36
37
|
system = scoped_index("system")
|
|
37
38
|
return [
|
|
38
39
|
"Project memories:",
|
|
@@ -79,19 +80,25 @@ module Samagotchi
|
|
|
79
80
|
missing << name unless found
|
|
80
81
|
end
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
end
|
|
83
|
+
return results.join(SEPARATOR) if missing.empty?
|
|
84
|
+
|
|
85
|
+
# The hint goes after the first line: WorkingSet reads the missing
|
|
86
|
+
# names off it (WorkingSet::MISSING) and a failure off its "Error".
|
|
87
|
+
miss = "Error: memory not found: #{missing.join(", ")}#{orphan_hint if resolved_scopes.include?("project")}"
|
|
88
|
+
results.empty? ? miss : results.join(SEPARATOR) + SEPARATOR + miss
|
|
89
89
|
rescue Errno::ENOENT => e
|
|
90
90
|
"Error: #{e.message}"
|
|
91
91
|
rescue StandardError => e
|
|
92
92
|
"Error: #{e.message}"
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
# "\nHint: …" when this project's memory folder looks orphaned
|
|
96
|
+
# (MemoryOrphans), else "".
|
|
97
|
+
def self.orphan_hint
|
|
98
|
+
message = MemoryOrphans.message
|
|
99
|
+
message ? "\nHint: #{message}" : ""
|
|
100
|
+
end
|
|
101
|
+
|
|
95
102
|
# A memory is a flat file in its scope dir: a name with a path in it
|
|
96
103
|
# ("../../x") would read or write outside the memories.
|
|
97
104
|
def self.invalid_name?(name)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "client_id"
|
|
4
|
+
require_relative "steer"
|
|
5
|
+
|
|
6
|
+
module Samagotchi
|
|
7
|
+
# Who queued a turn: the client that sent it (+client_id+, ClientId), the
|
|
8
|
+
# input file's id (+enqueued_id+, the reply's and the events' key) and,
|
|
9
|
+
# for a participant's, its +author+ ("p:<id>"; none for the owner's).
|
|
10
|
+
# Engine#run_turn's, the Worker's and GuardrailWiring's. On the wire (a
|
|
11
|
+
# turn's boundary events, Bridge SSE, the inbox JSON's own fields) an
|
|
12
|
+
# origin stays the Hash it always was: #to_h writes it, .from reads it
|
|
13
|
+
# back, so a value that crosses a boundary is turned there, once.
|
|
14
|
+
TurnOrigin = Data.define(:client_id, :enqueued_id, :author) do
|
|
15
|
+
def initialize(client_id: nil, enqueued_id: nil, author: nil) = super
|
|
16
|
+
|
|
17
|
+
# An origin from what a boundary carries: a TurnOrigin as it is, a Hash
|
|
18
|
+
# (symbol or string keys: an event, an SSE line, a spec) as one; nil for
|
|
19
|
+
# nil, an empty Hash and anything else (no sender named).
|
|
20
|
+
# @return [TurnOrigin, nil]
|
|
21
|
+
def self.from(value)
|
|
22
|
+
return value if value.is_a?(TurnOrigin)
|
|
23
|
+
return nil unless value.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
field = ->(key) { value.key?(key) ? value[key] : value[key.to_s] }
|
|
26
|
+
origin = new(client_id: field.call(:client_id), enqueued_id: field.call(:enqueued_id), author: field.call(:author))
|
|
27
|
+
origin.empty? ? nil : origin
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# An origin either way: .from's, or NONE when the value names no
|
|
31
|
+
# sender (nil, {}). In-process an owner's turn has NONE, never nil.
|
|
32
|
+
# @return [TurnOrigin]
|
|
33
|
+
def self.of(value) = from(value) || self::NONE
|
|
34
|
+
|
|
35
|
+
# Whether it names no sender (NONE): its wire form is left out.
|
|
36
|
+
def empty? = to_h.empty?
|
|
37
|
+
|
|
38
|
+
# The wire form where a field may carry no origin: #to_h, nil for NONE
|
|
39
|
+
# (an event's origin:, a hook's context).
|
|
40
|
+
# @return [Hash, nil]
|
|
41
|
+
def wire = empty? ? nil : to_h
|
|
42
|
+
|
|
43
|
+
# Whether a participant queued the turn (Steer.participant_author?: any
|
|
44
|
+
# author but none or the owner's).
|
|
45
|
+
def participant? = Steer.participant_author?(author)
|
|
46
|
+
|
|
47
|
+
# The author when a participant's (Steer.sender_author), else nil.
|
|
48
|
+
def participant_author = Steer.sender_author(author)
|
|
49
|
+
|
|
50
|
+
# The participant's id (Steer.participant_id: its author without "p:"),
|
|
51
|
+
# nil for the owner's.
|
|
52
|
+
def participant_id = participant_author && Steer.participant_id(participant_author)
|
|
53
|
+
|
|
54
|
+
# What sent the turn, by its client (ClientId.origin): "client",
|
|
55
|
+
# "reminder", "delegate", "review"...
|
|
56
|
+
def source = ClientId.origin(client_id)
|
|
57
|
+
|
|
58
|
+
# Whether a human sent the turn (ClientId.human?: a missing client is).
|
|
59
|
+
def human? = ClientId.human?(client_id)
|
|
60
|
+
|
|
61
|
+
# The wire form: only the fields set, as a turn's origin always went out.
|
|
62
|
+
def to_h = super.compact
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# No sender named: an owner's turn with no client, run_turn's default
|
|
66
|
+
# (TurnOrigin.of).
|
|
67
|
+
TurnOrigin::NONE = TurnOrigin.new
|
|
68
|
+
end
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -57,6 +57,7 @@ require_relative "session_summary"
|
|
|
57
57
|
require_relative "session_access_routes"
|
|
58
58
|
require_relative "principal"
|
|
59
59
|
require_relative "participant_access"
|
|
60
|
+
require_relative "participants_admin"
|
|
60
61
|
require_relative "participant_review"
|
|
61
62
|
require_relative "../log"
|
|
62
63
|
require_relative "../paths"
|
|
@@ -76,6 +77,7 @@ module Samagotchi
|
|
|
76
77
|
include RedactedInspect
|
|
77
78
|
include SessionAccessRoutes
|
|
78
79
|
include ParticipantAccess
|
|
80
|
+
include ParticipantsAdmin
|
|
79
81
|
include ParticipantReview
|
|
80
82
|
|
|
81
83
|
DEFAULT_HOST = BridgeClient::HOST
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "json"
|
|
6
|
+
require_relative "../steer"
|
|
6
7
|
require "securerandom"
|
|
7
8
|
require "time"
|
|
8
9
|
require "rack/utils"
|
|
@@ -104,7 +105,7 @@ module Samagotchi
|
|
|
104
105
|
def queue_limit = queue.is_a?(Integer) ? queue : Grants.default_queue
|
|
105
106
|
|
|
106
107
|
# The author a participant's turn carries (Principal#author).
|
|
107
|
-
def author =
|
|
108
|
+
def author = Steer.author_for(id)
|
|
108
109
|
|
|
109
110
|
# :active, :revoked or :expired at +now+ (a Time).
|
|
110
111
|
def state(now)
|