samagotchi 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -1
- data/docs/context.md +3 -1
- data/docs/hooks.md +6 -3
- data/docs/plugins.md +2 -1
- data/docs/releasing.md +2 -1
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +1 -15
- data/lib/samagotchi/bridge.rb +8 -3
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +31 -10
- data/lib/samagotchi/client_id.rb +52 -0
- data/lib/samagotchi/context_absorber.rb +2 -1
- data/lib/samagotchi/context_fetch.rb +14 -45
- data/lib/samagotchi/context_sources.rb +20 -10
- data/lib/samagotchi/engine.rb +10 -7
- data/lib/samagotchi/guardrails/parent_approvals.rb +0 -2
- data/lib/samagotchi/memory_bundle/builder.rb +22 -1
- data/lib/samagotchi/memory_bundle/manifest.rb +3 -1
- data/lib/samagotchi/plugin/sessions.rb +2 -1
- data/lib/samagotchi/process_group.rb +160 -0
- data/lib/samagotchi/prompt_warmup.rb +20 -1
- data/lib/samagotchi/question_desk.rb +2 -1
- data/lib/samagotchi/relay_verifier.rb +2 -4
- data/lib/samagotchi/reply_wait.rb +9 -4
- data/lib/samagotchi/send_command.rb +2 -1
- data/lib/samagotchi/session_manager.rb +2 -4
- data/lib/samagotchi/steer.rb +27 -19
- data/lib/samagotchi/terminal_ui/attached_loop.rb +5 -4
- data/lib/samagotchi/terminal_ui/formatting.rb +21 -12
- data/lib/samagotchi/terminal_ui.rb +2 -1
- data/lib/samagotchi/tools/delegate.rb +2 -2
- data/lib/samagotchi/tools/execute.rb +10 -81
- data/lib/samagotchi/tools/task_runtime.rb +25 -38
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +2 -1
- data/lib/samagotchi/web/public/app.js +19 -20
- data/lib/samagotchi/web/public/data.js +3 -0
- data/lib/samagotchi/web/public/question_card.js +3 -4
- data/lib/samagotchi/web/public/shown_texts.js +13 -0
- data/lib/samagotchi/web/public/stable_html.js +18 -0
- data/lib/samagotchi/web/public/turn_events.js +9 -2
- data/lib/samagotchi/web/public/turn_view.js +1 -1
- data/lib/samagotchi/worker.rb +33 -11
- metadata +5 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../client_id"
|
|
3
4
|
require_relative "../session"
|
|
4
5
|
require_relative "../bridge_client"
|
|
5
6
|
require_relative "../bridge/turn_accumulator"
|
|
@@ -62,7 +63,7 @@ module Samagotchi
|
|
|
62
63
|
|
|
63
64
|
state_dir = self.state_dir
|
|
64
65
|
sid = resolve(id, state_dir)
|
|
65
|
-
delivered = SessionManager.deliver_turn(sid, prompt: text.to_s, client_id:
|
|
66
|
+
delivered = SessionManager.deliver_turn(sid, prompt: text.to_s, client_id: ClientId::PLUGIN, state_dir: state_dir)
|
|
66
67
|
case delivered[:status]
|
|
67
68
|
when :accepted then sid
|
|
68
69
|
when :refused then raise Error, "session #{sid[0, 8]} refused the message (#{delivered.dig(:ack, "error")})"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# A command run in a process group of its own, so a stop reaches what it
|
|
5
|
+
# started too: TERM the group, a grace, then KILL it. Execute, TaskRuntime
|
|
6
|
+
# and ContextFetch spawn and stop their commands here, each with its own
|
|
7
|
+
# grace and its own "has it stopped?" check (the leader gone, the pipes
|
|
8
|
+
# closed, the pid reaped).
|
|
9
|
+
#
|
|
10
|
+
# The mcp bundle's client keeps its own copy (stdin closed first, then
|
|
11
|
+
# the group): a bundle may use chi's classes only from the chi its
|
|
12
|
+
# requires_chi names, and no released chi has this one yet.
|
|
13
|
+
module ProcessGroup
|
|
14
|
+
# A group that is gone, or one that isn't ours to signal.
|
|
15
|
+
QUIET = [Errno::ESRCH, Errno::EPERM].freeze
|
|
16
|
+
READ_CHUNK_BYTES = 65_536
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Process.spawn in a new process group, whose id is the pid.
|
|
21
|
+
# @return [Integer] the pid
|
|
22
|
+
def spawn(env, *argv, **) = Process.spawn(env, *argv, pgroup: true, **)
|
|
23
|
+
|
|
24
|
+
# Sends +sig+ to every process in the group +pgid+.
|
|
25
|
+
# @param leader [Boolean] when the group can't be signalled, signal the
|
|
26
|
+
# process +pgid+ alone (a leader that left its group)
|
|
27
|
+
# @param quiet [Array<Class>] the errors that mean there is nothing to
|
|
28
|
+
# signal; any other is raised
|
|
29
|
+
# @return [Boolean] whether a signal was sent
|
|
30
|
+
def signal(pgid, sig, leader: false, quiet: QUIET)
|
|
31
|
+
check!(pgid)
|
|
32
|
+
Process.kill(sig, -pgid)
|
|
33
|
+
true
|
|
34
|
+
rescue *quiet
|
|
35
|
+
return false unless leader
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
Process.kill(sig, pgid)
|
|
39
|
+
true
|
|
40
|
+
rescue *quiet
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A pid chi may signal or ask about: an Integer above 1. kill(0) and
|
|
46
|
+
# kill(-1) reach chi's own group and every process of the user, so a
|
|
47
|
+
# pid that isn't one (a broken or edited record) never gets that far.
|
|
48
|
+
def signalable?(pid) = pid.is_a?(Integer) && pid > 1
|
|
49
|
+
|
|
50
|
+
# @raise [ArgumentError] unless #signalable?
|
|
51
|
+
def check!(pid)
|
|
52
|
+
raise ArgumentError, "not a process group chi may signal: #{pid.inspect}" unless signalable?(pid)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Whether +pid+ runs and leads its own group, as #spawn's process does
|
|
56
|
+
# (not, then, a reused pid in some other group).
|
|
57
|
+
def leader?(pid)
|
|
58
|
+
signalable?(pid) && Process.getpgid(pid) == pid
|
|
59
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Whether +pid+ is still running (or a zombie not reaped yet).
|
|
64
|
+
# @raise [ArgumentError] unless #signalable?
|
|
65
|
+
def alive?(pid)
|
|
66
|
+
check!(pid)
|
|
67
|
+
Process.kill(0, pid)
|
|
68
|
+
true
|
|
69
|
+
rescue Errno::ESRCH
|
|
70
|
+
false
|
|
71
|
+
rescue Errno::EPERM
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# TERMs the group, waits up to +grace+ for +stopped+, then KILLs it.
|
|
76
|
+
# @param stopped [#call] true once it has stopped (default: the leader
|
|
77
|
+
# is gone)
|
|
78
|
+
# @param wait [#call] (seconds) between two checks (default: a sleep)
|
|
79
|
+
# @param signal_options [Hash] for #signal (leader:, quiet:)
|
|
80
|
+
# @return [Boolean] true when it stopped within the grace (no KILL sent)
|
|
81
|
+
# @raise [ArgumentError] unless #signalable?
|
|
82
|
+
def stop(pgid, grace:, poll:, stopped: -> { !alive?(pgid) }, wait: ->(seconds) { sleep(seconds) }, **signal_options)
|
|
83
|
+
check!(pgid)
|
|
84
|
+
signal(pgid, "TERM", **signal_options)
|
|
85
|
+
deadline = monotonic + grace
|
|
86
|
+
until (done = stopped.call) || monotonic >= deadline
|
|
87
|
+
wait.call(poll)
|
|
88
|
+
end
|
|
89
|
+
return true if done
|
|
90
|
+
|
|
91
|
+
signal(pgid, "KILL", **signal_options)
|
|
92
|
+
false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
96
|
+
|
|
97
|
+
# Reads a pipe in chunks on a thread of its own, so the output read so
|
|
98
|
+
# far survives the pipe being closed under it (or the thread killed).
|
|
99
|
+
class PipeReader
|
|
100
|
+
# @param cap [Integer, nil] the most bytes it keeps
|
|
101
|
+
# @param keep [:head, :tail] past the cap: :head stops reading (and
|
|
102
|
+
# #over? is true), :tail keeps the last +cap+ bytes
|
|
103
|
+
def initialize(io, cap: nil, keep: :head, chunk: READ_CHUNK_BYTES)
|
|
104
|
+
@io = io
|
|
105
|
+
@buffer = String.new(encoding: Encoding::BINARY)
|
|
106
|
+
@over = false
|
|
107
|
+
@thread = Thread.new do
|
|
108
|
+
Thread.current.report_on_exception = false
|
|
109
|
+
loop { break unless take(io.readpartial(chunk), cap, keep) }
|
|
110
|
+
rescue IOError # EOFError is one
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def done? = !@thread.alive?
|
|
116
|
+
|
|
117
|
+
# Whether a :head reader stopped at its cap.
|
|
118
|
+
def over? = @over
|
|
119
|
+
|
|
120
|
+
# @return [Thread, nil] the thread when it ended within +seconds+
|
|
121
|
+
def wait(seconds) = @thread.join(seconds)
|
|
122
|
+
|
|
123
|
+
def kill = @thread.kill
|
|
124
|
+
|
|
125
|
+
# Closes the pipe once the thread had +poll+ to end by itself (a
|
|
126
|
+
# reader still blocked gets IOError), then ends the thread, killing it
|
|
127
|
+
# after +grace+. The text is safe to read afterwards.
|
|
128
|
+
def finish(poll:, grace:)
|
|
129
|
+
@thread.join(poll) unless done?
|
|
130
|
+
close_quietly
|
|
131
|
+
@thread.kill unless @thread.join(grace)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# What it read, in +encoding+ (default: the pipe's).
|
|
135
|
+
def text(encoding = @io.external_encoding || Encoding.default_external)
|
|
136
|
+
@buffer.dup.force_encoding(encoding)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
# @return [Boolean] whether to read on
|
|
142
|
+
def take(chunk, cap, keep)
|
|
143
|
+
if cap && keep == :head && @buffer.bytesize + chunk.bytesize > cap
|
|
144
|
+
@over = true
|
|
145
|
+
return false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
@buffer << chunk
|
|
149
|
+
@buffer = @buffer.byteslice(-cap, cap) if cap && keep == :tail && @buffer.bytesize > cap
|
|
150
|
+
true
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def close_quietly
|
|
154
|
+
@io.close unless @io.closed?
|
|
155
|
+
rescue IOError
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -32,6 +32,8 @@ module Samagotchi
|
|
|
32
32
|
# Seconds #take_pin waits for the busy-slot check (Client#slot_status
|
|
33
33
|
# gives up within 3 s).
|
|
34
34
|
DECIDE_TIMEOUT = 3
|
|
35
|
+
# Seconds #stop waits for a killed warm-up's thread to end.
|
|
36
|
+
STOP_JOIN_SECONDS = 1
|
|
35
37
|
|
|
36
38
|
# Whether cache.warmup allows a warm-up (auto, the default; off, false,
|
|
37
39
|
# no or 0 turn it off; anything else warns once and is auto).
|
|
@@ -54,13 +56,16 @@ module Samagotchi
|
|
|
54
56
|
# The pin (#take_pin forgets it) and the last warm-up's thread.
|
|
55
57
|
@state = nil
|
|
56
58
|
@thread = nil
|
|
59
|
+
@stopped = false
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
# Start a warm-up of +prompt+ on +client+ in its own thread.
|
|
60
63
|
# @param slot [Integer, nil] the last request's slot (nil: unpinned, and
|
|
61
64
|
# nothing to pin the next request to)
|
|
62
|
-
# @return [Thread]
|
|
65
|
+
# @return [Thread, nil] nil once #stop ran
|
|
63
66
|
def start(client:, prompt:, model:, slot:, images: [])
|
|
67
|
+
return nil if @mutex.synchronize { @stopped }
|
|
68
|
+
|
|
64
69
|
# Whether the warm-up went to its slot (true) or was skipped: the
|
|
65
70
|
# first value pushed is the answer.
|
|
66
71
|
sent = Queue.new
|
|
@@ -111,6 +116,20 @@ module Samagotchi
|
|
|
111
116
|
thread&.alive? || false
|
|
112
117
|
end
|
|
113
118
|
|
|
119
|
+
# Ends a warm-up still running and starts none after (Engine#shutdown:
|
|
120
|
+
# no next turn will use it). Its request is cut, which frees the slot.
|
|
121
|
+
def stop(timeout = STOP_JOIN_SECONDS)
|
|
122
|
+
thread = @mutex.synchronize do
|
|
123
|
+
@stopped = true
|
|
124
|
+
@state = nil
|
|
125
|
+
@thread
|
|
126
|
+
end
|
|
127
|
+
return unless thread&.alive?
|
|
128
|
+
|
|
129
|
+
thread.kill
|
|
130
|
+
thread.join(timeout)
|
|
131
|
+
end
|
|
132
|
+
|
|
114
133
|
# Wait up to +timeout+ seconds for the running warm-up (specs).
|
|
115
134
|
def wait(timeout = nil)
|
|
116
135
|
thread = @mutex.synchronize { @thread }
|
|
@@ -5,6 +5,7 @@ require "monitor"
|
|
|
5
5
|
require "securerandom"
|
|
6
6
|
require "time"
|
|
7
7
|
|
|
8
|
+
require_relative "client_id"
|
|
8
9
|
require_relative "log"
|
|
9
10
|
require_relative "tools/ask_user_question"
|
|
10
11
|
require_relative "guardrails/parent_approvals"
|
|
@@ -266,7 +267,7 @@ module Samagotchi
|
|
|
266
267
|
sel = Array(selected).map { |v| v.to_s.strip }.reject(&:empty?)
|
|
267
268
|
fm = freeform.to_s.strip
|
|
268
269
|
fm = nil if fm.empty?
|
|
269
|
-
parent_agent = client_id.to_s ==
|
|
270
|
+
parent_agent = client_id.to_s == ClientId::CLI_ANSWER if parent_agent.nil?
|
|
270
271
|
# Read before the lock (config may touch the disk); this worker's own.
|
|
271
272
|
parent_setting = Guardrails::ParentApprovals.setting if parent_agent
|
|
272
273
|
parent_continue = Guardrails::ParentContinue.allowed? if parent_agent
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "client_id"
|
|
3
4
|
require_relative "session"
|
|
4
5
|
require_relative "bridge_client"
|
|
5
6
|
require_relative "worker_sidecar"
|
|
@@ -15,9 +16,6 @@ module Samagotchi
|
|
|
15
16
|
# answers its own question with that, only when it is about this session
|
|
16
17
|
# and the question pending now, and is answered.
|
|
17
18
|
class RelayVerifier
|
|
18
|
-
# The client id a relayed answer is recorded with: relay:<parent8>.
|
|
19
|
-
CLIENT_PREFIX = "relay"
|
|
20
|
-
|
|
21
19
|
# @param engine [Engine] this (the child's) worker's Engine
|
|
22
20
|
def initialize(engine:, state_dir:, session_id:)
|
|
23
21
|
@engine = engine
|
|
@@ -44,7 +42,7 @@ module Samagotchi
|
|
|
44
42
|
def apply(relay, parent_id:, question_id:, relay_id:)
|
|
45
43
|
answer = relay["answer"].is_a?(Hash) ? relay["answer"] : {}
|
|
46
44
|
parent_agent = relay["by"] != "user"
|
|
47
|
-
client_id = "#{
|
|
45
|
+
client_id = "#{ClientId::RELAY_PREFIX}#{parent_id[0, 8]}"
|
|
48
46
|
if answer["dismissed"]
|
|
49
47
|
return not_pending(question_id) unless @engine.cancel_question("dismissed", id: question_id)
|
|
50
48
|
|
|
@@ -62,8 +62,9 @@ module Samagotchi
|
|
|
62
62
|
# question already pending then is not the answer's. A nil
|
|
63
63
|
# messages: skips the count. last_turn: (the session's
|
|
64
64
|
# last_turn["ended_at"] then, maybe nil) ends the wait on any turn
|
|
65
|
-
# that ended since, idle again, however fast it ran
|
|
66
|
-
# a failure replaces the note and leaves the count as it was
|
|
65
|
+
# that ended since, idle again, however fast it ran (a failure after
|
|
66
|
+
# a failure replaces the note and leaves the count as it was), and
|
|
67
|
+
# with it the count isn't used: a note between turns grows it too.
|
|
67
68
|
# @param owner_grace [Numeric, nil] seconds with no live worker before
|
|
68
69
|
# :worker_gone (one that died before its rescue leaves it running)
|
|
69
70
|
# @param interject [#call, nil] called once a poll, before the sleep
|
|
@@ -145,13 +146,17 @@ module Samagotchi
|
|
|
145
146
|
end
|
|
146
147
|
|
|
147
148
|
# An idle session whose turn ended after the baseline was taken: its
|
|
148
|
-
# last_turn moved on
|
|
149
|
+
# last_turn moved on. Every turn's end records last_turn as it goes
|
|
150
|
+
# idle (Engine#end_turn), so with one in the baseline that alone
|
|
151
|
+
# decides: messages also grow between turns (a new worker's context
|
|
152
|
+
# note, a chi note) with no turn run. A baseline without it falls back
|
|
153
|
+
# to the count (a turn's note).
|
|
149
154
|
def turn_ended_since?(session, baseline)
|
|
150
155
|
return false unless baseline
|
|
151
156
|
|
|
152
157
|
if baseline.key?(:last_turn)
|
|
153
158
|
ended = session.last_turn.is_a?(Hash) ? session.last_turn["ended_at"] : nil
|
|
154
|
-
return
|
|
159
|
+
return !ended.nil? && ended != baseline[:last_turn]
|
|
155
160
|
end
|
|
156
161
|
!!(baseline[:messages] && session.messages.size > baseline[:messages])
|
|
157
162
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "client_id"
|
|
3
4
|
require_relative "session"
|
|
4
5
|
require_relative "session_inbox"
|
|
5
6
|
require_relative "session_manager"
|
|
@@ -26,7 +27,7 @@ module Samagotchi
|
|
|
26
27
|
include CLI::Command
|
|
27
28
|
include CLI::ParentWait
|
|
28
29
|
|
|
29
|
-
CLIENT_ID =
|
|
30
|
+
CLIENT_ID = ClientId::CLI_SEND
|
|
30
31
|
# The cap on one turn's images the Bridge checks.
|
|
31
32
|
MAX_IMAGES = ImageStore::MAX_TURN_REFS
|
|
32
33
|
|
|
@@ -5,6 +5,7 @@ require "json"
|
|
|
5
5
|
require "time"
|
|
6
6
|
require "securerandom"
|
|
7
7
|
require "rbconfig"
|
|
8
|
+
require_relative "client_id"
|
|
8
9
|
require_relative "atomic_file"
|
|
9
10
|
require_relative "config"
|
|
10
11
|
|
|
@@ -59,9 +60,6 @@ module Samagotchi
|
|
|
59
60
|
autoload :SessionRetention, File.expand_path("session_retention", __dir__)
|
|
60
61
|
|
|
61
62
|
class SessionManager
|
|
62
|
-
# Origin of the synthetic turn queued when reminders are due.
|
|
63
|
-
REMINDER_CLIENT_ID = "system:reminder"
|
|
64
|
-
|
|
65
63
|
# Raised when the interactive TUI owns the session: it runs its own Engine
|
|
66
64
|
# and reads no input files, so a worker must not be spawned or signalled.
|
|
67
65
|
class OwnedByTUI < StandardError
|
|
@@ -583,7 +581,7 @@ module Samagotchi
|
|
|
583
581
|
# @raise [OwnedByTUI] a chi REPL owns the session
|
|
584
582
|
# @raise [RestartRefused]
|
|
585
583
|
# @raise [ArgumentError] no such session
|
|
586
|
-
def self.restart_session(session_id, state_dir: nil, wait: RESTART_WAIT, client_id:
|
|
584
|
+
def self.restart_session(session_id, state_dir: nil, wait: RESTART_WAIT, client_id: ClientId::CLI_RESTART)
|
|
587
585
|
sd = state_dir || Session.default_state_dir
|
|
588
586
|
Session.load(session_id, state_dir: sd) # ArgumentError for none
|
|
589
587
|
owner = refuse_tui!(session_id, state_dir: sd)
|
data/lib/samagotchi/steer.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "client_id"
|
|
4
|
+
|
|
3
5
|
module Samagotchi
|
|
4
6
|
# A plugin's steer: text a plugin puts into the running turn (Engine#steer),
|
|
5
7
|
# the way a UI's steering does. The drain a loop gets returns items: a
|
|
@@ -12,8 +14,9 @@ module Samagotchi
|
|
|
12
14
|
# "input": they are part of that turn, not a turn of their own, so a
|
|
13
15
|
# reloaded history keeps the turn in one piece. A line can be a Line
|
|
14
16
|
# naming who sent it (a worker's input from chi send, a delegating parent,
|
|
15
|
-
# a plugin); its input message then carries
|
|
16
|
-
# lines carry none, so their saved bytes are
|
|
17
|
+
# a plugin, a client chi doesn't know); its input message then carries
|
|
18
|
+
# that source:. The user's own lines carry none, so their saved bytes are
|
|
19
|
+
# as they always were.
|
|
17
20
|
module Steer
|
|
18
21
|
KIND = "steer"
|
|
19
22
|
INPUT_KIND = "input"
|
|
@@ -27,18 +30,10 @@ module Samagotchi
|
|
|
27
30
|
def initialize(text:, source:, mark: nil) = super
|
|
28
31
|
end
|
|
29
32
|
|
|
30
|
-
# The client ids whose input is not the user's own words. Literals, not
|
|
31
|
-
# SendCommand::CLIENT_ID / Tools::Delegate::CLIENT_PREFIX, so this file
|
|
32
|
-
# needs no requires (a spec pins them equal).
|
|
33
|
-
CHI_SEND_CLIENT = "cli:send"
|
|
34
|
-
DELEGATE_CLIENT_PREFIX = "delegate:"
|
|
35
|
-
# A delegate child's report (ChildReports::CLIENT_PREFIX).
|
|
36
|
-
CHILD_CLIENT_PREFIX = "child:"
|
|
37
|
-
# A turn chi ran because an attached context source changed
|
|
38
|
-
# (context:<name>, Worker's context wake).
|
|
39
|
-
CONTEXT_CLIENT_PREFIX = "context:"
|
|
40
33
|
DELEGATE_REPORT = "delegate_report"
|
|
41
|
-
|
|
34
|
+
# The source of a line from a client that is not a human's
|
|
35
|
+
# (ClientId.human?) and not one chi names: automatic:<client id>.
|
|
36
|
+
AUTOMATIC_SOURCE_PREFIX = "automatic:"
|
|
42
37
|
|
|
43
38
|
# What one drain brought: the merged user text (nil when none) and how
|
|
44
39
|
# many lines made it, the input messages' text grouped by sender
|
|
@@ -98,35 +93,48 @@ module Samagotchi
|
|
|
98
93
|
"Don't call delegate_result for it; to retry or redirect that child, use delegate with " \
|
|
99
94
|
"its session:.]"
|
|
100
95
|
}.freeze
|
|
96
|
+
# A line from a client chi doesn't know (AUTOMATIC_SOURCE_PREFIX):
|
|
97
|
+
# never taken for the user's.
|
|
98
|
+
AUTOMATIC_HEADER = "[Automatic input from %s, sent mid-task; not your user's message. " \
|
|
99
|
+
"If it asks for nothing, carry on with the task.]"
|
|
101
100
|
|
|
102
101
|
module_function
|
|
103
102
|
|
|
104
103
|
# The saved source of a worker input line from +client_id+: nil for the
|
|
105
|
-
# user (no client id, the web, an attached TUI
|
|
104
|
+
# user (no client id, the web, an attached TUI); automatic:<client id>
|
|
105
|
+
# for a client that is no human's and chi doesn't name (an unknown id is
|
|
106
|
+
# not the user).
|
|
106
107
|
def source_for_client(client_id)
|
|
107
108
|
id = client_id.to_s
|
|
108
|
-
if id ==
|
|
109
|
-
elsif id.start_with?(
|
|
110
|
-
elsif id.start_with?(
|
|
111
|
-
elsif id ==
|
|
109
|
+
if id == ClientId::CLI_SEND then "chi_send"
|
|
110
|
+
elsif id.start_with?(ClientId::DELEGATE_PREFIX) then "parent_agent"
|
|
111
|
+
elsif id.start_with?(ClientId::CHILD_PREFIX) then DELEGATE_REPORT
|
|
112
|
+
elsif id == ClientId::PLUGIN then "plugin_send"
|
|
113
|
+
elsif !ClientId.human?(client_id) then "#{AUTOMATIC_SOURCE_PREFIX}#{id}"
|
|
112
114
|
end
|
|
113
115
|
end
|
|
114
116
|
|
|
115
117
|
# The header for a steer or merged input message, else nil. No source
|
|
116
118
|
# (or "") is the user's; an input with an unknown source too; a steer
|
|
117
|
-
# with an unknown source is a plugin's (its bundle or hook label)
|
|
119
|
+
# with an unknown source is a plugin's (its bundle or hook label); an
|
|
120
|
+
# automatic:<client id> source names that client, not the user.
|
|
118
121
|
def header(message)
|
|
119
122
|
kind = (message[:kind] || message["kind"]).to_s
|
|
120
123
|
return unless [KIND, INPUT_KIND].include?(kind)
|
|
121
124
|
|
|
122
125
|
source = (message[:source] || message["source"]).to_s
|
|
123
126
|
return HEADERS[source] if HEADERS.key?(source)
|
|
127
|
+
return automatic_header(source.delete_prefix(AUTOMATIC_SOURCE_PREFIX)) if source.start_with?(AUTOMATIC_SOURCE_PREFIX)
|
|
124
128
|
|
|
125
129
|
source = "user" if source.empty?
|
|
126
130
|
sender = SENDERS[source] || (kind == KIND ? "from the #{source} plugin" : SENDERS["user"])
|
|
127
131
|
"[Steer #{sender}, #{HEADER_TAIL}"
|
|
128
132
|
end
|
|
129
133
|
|
|
134
|
+
def automatic_header(client_id)
|
|
135
|
+
format(AUTOMATIC_HEADER, client_id.empty? ? "an unnamed client" : client_id)
|
|
136
|
+
end
|
|
137
|
+
|
|
130
138
|
# The text a steer or merged input goes out as: its header, a newline,
|
|
131
139
|
# then +text+ (the message's content by default). Other messages: +text+.
|
|
132
140
|
def wire_text(message, text = (message[:content] || message["content"]).to_s)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../client_id"
|
|
3
4
|
require_relative "event_renderer"
|
|
4
5
|
require_relative "formatting"
|
|
5
6
|
require_relative "../prompt_history"
|
|
@@ -773,7 +774,7 @@ module Samagotchi
|
|
|
773
774
|
return @screen.commit(answer.error)
|
|
774
775
|
end
|
|
775
776
|
|
|
776
|
-
parent = @parent_answers ? { client_id:
|
|
777
|
+
parent = @parent_answers ? { client_id: ClientId::CLI_ANSWER } : {}
|
|
777
778
|
reply = @client.answer(id: @question.id, selected: answer.selected, freeform: answer.freeform, **parent)
|
|
778
779
|
case reply.status
|
|
779
780
|
when 200
|
|
@@ -1268,9 +1269,9 @@ module Samagotchi
|
|
|
1268
1269
|
nil
|
|
1269
1270
|
end
|
|
1270
1271
|
|
|
1271
|
-
def reminder_origin?(origin) = origin[:client_id].to_s.start_with?(
|
|
1272
|
-
def report_origin?(origin) = origin[:client_id].to_s.start_with?(
|
|
1273
|
-
def context_origin?(origin) = origin[:client_id].to_s.start_with?(
|
|
1272
|
+
def reminder_origin?(origin) = origin[:client_id].to_s.start_with?(ClientId::SYSTEM_PREFIX)
|
|
1273
|
+
def report_origin?(origin) = origin[:client_id].to_s.start_with?(ClientId::CHILD_PREFIX)
|
|
1274
|
+
def context_origin?(origin) = origin[:client_id].to_s.start_with?(ClientId::CONTEXT_PREFIX)
|
|
1274
1275
|
# A turn with no prompt: "(continuing)"; a context wake's label alone.
|
|
1275
1276
|
def continuing_text(origin) = context_origin?(origin) ? nil : "(continuing)"
|
|
1276
1277
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../client_id"
|
|
3
4
|
require_relative "../served_model"
|
|
4
5
|
require_relative "../image_store"
|
|
5
6
|
require_relative "../turn_note"
|
|
@@ -113,21 +114,29 @@ module Samagotchi
|
|
|
113
114
|
" #{paint("+#{get.call(:added).to_i}", 32)} #{paint("\u2212#{get.call(:removed).to_i}", 31)}"
|
|
114
115
|
end
|
|
115
116
|
|
|
116
|
-
# Prompt labels by the sender's client_id prefix
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
#
|
|
117
|
+
# Prompt labels by the sender's client_id: a whole id, or a prefix
|
|
118
|
+
# (ending in ":") it starts with (turn_events.js CLIENT_LABELS;
|
|
119
|
+
# spec/shared/labels_matrix.json).
|
|
120
|
+
CLIENT_LABELS = { ClientId::WEB_PREFIX => "web", ClientId::TUI_PREFIX => "tui", ClientId::SYSTEM_PREFIX => "reminder",
|
|
121
|
+
ClientId::DELEGATE_PREFIX => "delegate", ClientId::CHILD_PREFIX => "delegate report",
|
|
122
|
+
ClientId::CLI_SEND => "chi send", ClientId::CLI_ANSWER => "chi answer",
|
|
123
|
+
ClientId::PLUGIN => "plugin" }.freeze
|
|
124
|
+
# A client id chi doesn't know: not the user (ClientId.human?).
|
|
125
|
+
AUTOMATIC_LABEL = "automatic"
|
|
126
|
+
|
|
127
|
+
# Who sent a line, by its client_id: CLIENT_LABELS by the whole id or
|
|
128
|
+
# its prefix, a turn an attached context source started
|
|
129
|
+
# (context:<name>) as "context <name> changed", AUTOMATIC_LABEL for
|
|
130
|
+
# an id chi doesn't know; +fallback+ for none (the user's own).
|
|
124
131
|
def client_label(client_id, fallback = "user")
|
|
125
132
|
return fallback unless client_id
|
|
126
133
|
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
id = client_id.to_s
|
|
135
|
+
name = id.delete_prefix(ClientId::CONTEXT_PREFIX)
|
|
136
|
+
return "context #{name} changed" if id.start_with?(ClientId::CONTEXT_PREFIX) && !name.empty?
|
|
129
137
|
|
|
130
|
-
|
|
138
|
+
prefix = id[/\A[^:]*:/]
|
|
139
|
+
CLIENT_LABELS[id] || (prefix && CLIENT_LABELS[prefix]) || AUTOMATIC_LABEL
|
|
131
140
|
end
|
|
132
141
|
|
|
133
142
|
# "web> <prompt>": a prompt, labelled by who sent it; nil: the label alone.
|
|
@@ -142,7 +151,7 @@ module Samagotchi
|
|
|
142
151
|
status = report[/^status: (.*)$/, 1]
|
|
143
152
|
rest = report.split("\n").drop(2).reject { |line| line.strip.empty? || line == "---" }.first.to_s
|
|
144
153
|
rest = "#{rest[0, STEER_PREVIEW - 1]}…" if rest.length > STEER_PREVIEW
|
|
145
|
-
"#{paint("#{CLIENT_LABELS[
|
|
154
|
+
"#{paint("#{CLIENT_LABELS[ClientId::CHILD_PREFIX]}>", 35)} #{[id, status].compact.join(" ")}#{": #{rest}" unless rest.empty?}"
|
|
146
155
|
end
|
|
147
156
|
|
|
148
157
|
# The last prompt as a join shows it: a delegate report (a wake
|
|
@@ -5,6 +5,7 @@ require "fileutils"
|
|
|
5
5
|
require "io/console"
|
|
6
6
|
require "reline"
|
|
7
7
|
|
|
8
|
+
require_relative "client_id"
|
|
8
9
|
require_relative "iteration_limit"
|
|
9
10
|
require_relative "model_profile"
|
|
10
11
|
require_relative "turn_note"
|
|
@@ -1281,7 +1282,7 @@ module Samagotchi
|
|
|
1281
1282
|
end
|
|
1282
1283
|
|
|
1283
1284
|
begin
|
|
1284
|
-
parent = parent_answers? ? { client_id:
|
|
1285
|
+
parent = parent_answers? ? { client_id: ClientId::CLI_ANSWER } : {}
|
|
1285
1286
|
@engine.answer_question(id: prompt.id, selected: answer.selected, freeform: answer.freeform, **parent)
|
|
1286
1287
|
close_question_widget(prompt, prompt.answer_text(answer))
|
|
1287
1288
|
return true
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../client_id"
|
|
3
4
|
require_relative "../session"
|
|
4
5
|
require_relative "../config"
|
|
5
6
|
require_relative "../model_profile"
|
|
@@ -21,7 +22,6 @@ module Samagotchi
|
|
|
21
22
|
# reply comes back (DelegateWait), never its trace.
|
|
22
23
|
class Delegate
|
|
23
24
|
NAME = "delegate"
|
|
24
|
-
CLIENT_PREFIX = "delegate"
|
|
25
25
|
MAX_CHILDREN_KEY = "session.max_children"
|
|
26
26
|
MAX_CHILDREN_DEFAULT = 4
|
|
27
27
|
# A child spawned this many seconds ago counts as running even before
|
|
@@ -113,7 +113,7 @@ module Samagotchi
|
|
|
113
113
|
|
|
114
114
|
# Only a reply after this delivery counts as the follow-up's.
|
|
115
115
|
DelegateWait.mark_seen(parent.id, child, state_dir: state_dir)
|
|
116
|
-
delivered = SessionManager.deliver_turn(id, prompt: task, client_id: "#{
|
|
116
|
+
delivered = SessionManager.deliver_turn(id, prompt: task, client_id: "#{ClientId::DELEGATE_PREFIX}#{parent.id[0, 8]}",
|
|
117
117
|
state_dir: state_dir)
|
|
118
118
|
case delivered[:status]
|
|
119
119
|
when :accepted then id
|