samagotchi 0.102.0 → 0.103.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 +40 -1
- data/bin/chi +23 -191
- data/docs/architecture.md +54 -0
- data/docs/cli.md +114 -7
- data/docs/configuration.md +7 -1
- data/docs/guardrails.md +2 -1
- data/docs/internals/engine.md +1 -1
- data/docs/internals/llm-context-forget.md +11 -1
- data/docs/internals/remote-sessions.md +303 -0
- data/docs/internals/working-set.md +95 -11
- data/docs/plugins.md +26 -2
- data/docs/sessions.md +12 -5
- data/docs/sub-agent.md +8 -0
- data/lib/samagotchi/answer_command.rb +27 -25
- data/lib/samagotchi/bridge.rb +24 -4
- data/lib/samagotchi/bridge_client/event_stream.rb +81 -12
- data/lib/samagotchi/bridge_client/stream_request.rb +77 -0
- data/lib/samagotchi/bridge_client.rb +22 -38
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +30 -1
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +49 -4
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +7 -4
- data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/chi_invocation.rb +43 -0
- data/lib/samagotchi/cli/command.rb +3 -2
- data/lib/samagotchi/cli/flags.rb +5 -0
- data/lib/samagotchi/cli/parent_wait.rb +22 -13
- data/lib/samagotchi/context_command.rb +80 -73
- data/lib/samagotchi/engine.rb +12 -8
- data/lib/samagotchi/idle_recap.rb +7 -1
- data/lib/samagotchi/kernel_loop.rb +13 -1
- data/lib/samagotchi/launch_mode.rb +22 -0
- data/lib/samagotchi/llm_context_forget.rb +13 -6
- data/lib/samagotchi/note_command.rb +8 -7
- data/lib/samagotchi/parent_report.rb +55 -43
- data/lib/samagotchi/remote_command.rb +170 -0
- data/lib/samagotchi/remotes.rb +130 -0
- data/lib/samagotchi/run_options.rb +233 -0
- data/lib/samagotchi/send_command/busy.rb +58 -0
- data/lib/samagotchi/send_command/parent_gate.rb +23 -10
- data/lib/samagotchi/send_command/wait_target.rb +16 -3
- data/lib/samagotchi/send_command.rb +173 -108
- data/lib/samagotchi/session.rb +30 -5
- data/lib/samagotchi/session_access/contract.rb +129 -0
- data/lib/samagotchi/session_access/dispatch.rb +133 -0
- data/lib/samagotchi/session_access/http_transport.rb +109 -0
- data/lib/samagotchi/session_access/local.rb +349 -0
- data/lib/samagotchi/session_access/remote.rb +638 -0
- data/lib/samagotchi/session_access/web_bridge_client.rb +181 -0
- data/lib/samagotchi/session_access.rb +141 -0
- data/lib/samagotchi/session_archive_command.rb +9 -5
- data/lib/samagotchi/session_commands.rb +20 -15
- data/lib/samagotchi/session_delete_command.rb +8 -4
- data/lib/samagotchi/session_manager.rb +7 -2
- data/lib/samagotchi/session_setup.rb +11 -6
- data/lib/samagotchi/sessions_command.rb +36 -14
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +51 -15
- data/lib/samagotchi/terminal_ui/attach_link.rb +104 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +80 -75
- data/lib/samagotchi/terminal_ui/local_link.rb +130 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +5 -3
- data/lib/samagotchi/terminal_ui.rb +2 -2
- data/lib/samagotchi/title_command.rb +9 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +360 -69
- data/lib/samagotchi/web/public/data.js +3 -2
- data/lib/samagotchi/web/public/index.html +20 -4
- data/lib/samagotchi/web/public/stage_frame.js +7 -2
- data/lib/samagotchi/web/public/stage_view.js +3 -2
- data/lib/samagotchi/web/public/stream_handlers.js +3 -1
- data/lib/samagotchi/web/public/working_set.js +159 -28
- data/lib/samagotchi/web/public/working_set_rail.js +39 -12
- data/lib/samagotchi/web/public/working_set_view.js +91 -33
- data/lib/samagotchi/web/session_access_routes.rb +343 -0
- data/lib/samagotchi/worker_sidecar.rb +12 -5
- data/lib/samagotchi/working_set.rb +88 -14
- data/lib/samagotchi/working_set_command.rb +133 -30
- data/lib/samagotchi/working_set_pin.rb +34 -0
- data/lib/samagotchi/working_set_steer.rb +191 -27
- metadata +19 -1
|
@@ -47,18 +47,45 @@ module Samagotchi
|
|
|
47
47
|
# collapsed); +state+ live|stale|forgotten|dropped; +tokens+ its content
|
|
48
48
|
# outputs as sent; +ops+ read|edit for a file, read|write for a memory,
|
|
49
49
|
# read for a URL, in first-seen order; +count+ the runs that touched
|
|
50
|
-
# it; +first_id+ the first one's chi id (ToolIds)
|
|
51
|
-
|
|
50
|
+
# it; +first_id+ the first one's chi id (ToolIds); +pinned+ whether
|
|
51
|
+
# the user pinned it (WorkingSetPin: the model's forget_outputs refuses
|
|
52
|
+
# its outputs), whatever the strategy; +has_drop+ whether any of its
|
|
53
|
+
# content outputs holds the user's applied drop (an undrop has
|
|
54
|
+
# something to take back: a forgotten item too, its other outputs
|
|
55
|
+
# stubbed by the model's layers).
|
|
56
|
+
Item = Data.define(:kind, :key, :state, :tokens, :ops, :count, :first_id, :pinned, :has_drop)
|
|
57
|
+
|
|
58
|
+
# An item the user added (/working-set add) that the next turn's note
|
|
59
|
+
# asks the model to read: its identity (+kind+, +key+, as an Item's)
|
|
60
|
+
# and how the note names it (+label+: a file relative to the session's
|
|
61
|
+
# folder, "memory NAME", a URL).
|
|
62
|
+
Add = Data.define(:kind, :key, :label)
|
|
52
63
|
|
|
53
64
|
# A projection and the conversation's size (entries) it was built
|
|
54
65
|
# from: the web keeps the newer of an event's and a GET's by it.
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
# +adds+: the pending note's adds (#pending_adds), not items yet.
|
|
67
|
+
Snapshot = Data.define(:items, :at, :adds) do
|
|
68
|
+
def initialize(items:, at:, adds: [])
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def empty? = items.empty? && adds.empty?
|
|
57
73
|
|
|
58
|
-
# The JSON the event and the route carry: {items: [...], at:}
|
|
59
|
-
|
|
74
|
+
# The JSON the event and the route carry: {items: [...], at:}, and
|
|
75
|
+
# adds: [{kind, key, label}] while any is pending.
|
|
76
|
+
def to_h
|
|
77
|
+
hash = { items: items.map(&:to_h), at: at }
|
|
78
|
+
hash[:adds] = adds.map(&:to_h) unless adds.empty?
|
|
79
|
+
hash
|
|
80
|
+
end
|
|
60
81
|
end
|
|
61
82
|
|
|
83
|
+
# The turn-start note (WorkingSetSteer#record!): a tail system message
|
|
84
|
+
# of this kind, its ops ([{"op", "kind", "key", "label"}]) in NOTE_OPS.
|
|
85
|
+
NOTE_KIND = "working_set"
|
|
86
|
+
NOTE_OPS = :working_set_ops
|
|
87
|
+
ADD = "add"
|
|
88
|
+
|
|
62
89
|
FILE_OPS = { "read" => "read", "edit" => "edit", "write" => "edit" }.freeze
|
|
63
90
|
CONTENT = %w[read memory_read web_fetch].freeze
|
|
64
91
|
MEMORY_READ = Tools::MemoryRead::NAME
|
|
@@ -83,21 +110,52 @@ module Samagotchi
|
|
|
83
110
|
# @param strategy [Symbol, Array<Symbol>] :none or the layers, as
|
|
84
111
|
# LLMContextView takes it (the session's current one)
|
|
85
112
|
# @param stat [#call] File.stat, or a stand-in (specs)
|
|
113
|
+
# @param pins [Array<WorkingSetPin>] the session's pins
|
|
86
114
|
# @return [Snapshot]
|
|
87
|
-
def build(conversation, cwd:, strategy:, stat: File.method(:stat))
|
|
115
|
+
def build(conversation, cwd:, strategy:, stat: File.method(:stat), pins: [])
|
|
88
116
|
conversation = Array(conversation)
|
|
89
117
|
view = LLMContextView.new(strategy: strategy)
|
|
118
|
+
pinned = pins.to_set { |pin| [pin.kind, pin.key] }
|
|
90
119
|
items = all_touches(conversation, cwd, view).group_by { |touch| [touch.kind, touch.key] }.map do |(kind, key), group|
|
|
91
|
-
item(kind, key, group, conversation, view, stat)
|
|
120
|
+
item(kind, key, group, conversation, view, stat, pinned.include?([kind, key]))
|
|
121
|
+
end
|
|
122
|
+
Snapshot.new(items: items, at: conversation.size, adds: pending_adds(conversation))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The index of the note the next turn will send (one after the last
|
|
126
|
+
# turn's entries: only system messages follow it), or nil.
|
|
127
|
+
def pending_note_index(messages)
|
|
128
|
+
(messages.size - 1).downto(0) do |index|
|
|
129
|
+
message = messages[index]
|
|
130
|
+
return nil unless message.is_a?(Hash) && message[:role].to_s == "system"
|
|
131
|
+
return index if message[:kind].to_s == NOTE_KIND
|
|
132
|
+
end
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The ops the pending note lists ([{"op", "kind", "key", "label"}]).
|
|
137
|
+
def pending_ops(messages)
|
|
138
|
+
messages = Array(messages)
|
|
139
|
+
index = pending_note_index(messages)
|
|
140
|
+
index ? Array(messages[index][NOTE_OPS]).select { |op| op.is_a?(Hash) } : []
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The items the user added since the last turn, which the next turn's
|
|
144
|
+
# note asks the model to read.
|
|
145
|
+
# @return [Array<Add>]
|
|
146
|
+
def pending_adds(messages)
|
|
147
|
+
pending_ops(messages).select { |op| op["op"] == ADD }.map do |op|
|
|
148
|
+
Add.new(kind: op["kind"].to_s, key: op["key"].to_s, label: op["label"].to_s)
|
|
92
149
|
end
|
|
93
|
-
Snapshot.new(items: items, at: conversation.size)
|
|
94
150
|
end
|
|
95
151
|
|
|
96
152
|
# One content output (read, memory_read, web_fetch) of an item: its
|
|
97
153
|
# chi +id+ (ToolIds), the +index+ of the tool_response entry holding
|
|
98
154
|
# it, whether the id is +derived+ (a legacy entry's: nothing can name
|
|
99
155
|
# it) and whether the model is still +sent+ it under the strategy.
|
|
100
|
-
|
|
156
|
+
# +shared_with+: the other names a memory_read of several names holds in the
|
|
157
|
+
# same output (an op on the output acts on them too), else empty.
|
|
158
|
+
Output = Data.define(:id, :index, :derived, :sent, :shared_with)
|
|
101
159
|
|
|
102
160
|
# The content outputs of the item +kind+/+key+ (#build's identity: a
|
|
103
161
|
# file's absolute path, a memory's name, a URL), in conversation
|
|
@@ -113,10 +171,23 @@ module Samagotchi
|
|
|
113
171
|
|
|
114
172
|
ref = touch.run.ref
|
|
115
173
|
Output.new(id: ref.id, index: touch.run.index, derived: ref.derived?,
|
|
116
|
-
sent: still_sent?(touch, conversation, view))
|
|
174
|
+
sent: still_sent?(touch, conversation, view), shared_with: shared_with(touch, key))
|
|
117
175
|
end
|
|
118
176
|
end
|
|
119
177
|
|
|
178
|
+
# The ids of the content outputs of the items +pins+ name (the ones
|
|
179
|
+
# the model's forget_outputs refuses), by the same pairing as #build.
|
|
180
|
+
# @param pins [Array<WorkingSetPin>]
|
|
181
|
+
# @return [Set<String>]
|
|
182
|
+
def pinned_ids(conversation, pins:, cwd:)
|
|
183
|
+
return Set.new if pins.empty?
|
|
184
|
+
|
|
185
|
+
conversation = Array(conversation)
|
|
186
|
+
pinned = pins.to_set { |pin| [pin.kind, pin.key] }
|
|
187
|
+
touches = all_touches(conversation, cwd, LLMContextView.new(strategy: LLMContextView::NONE))
|
|
188
|
+
touches.select { |touch| touch.content && pinned.include?([touch.kind, touch.key]) }.to_set { |touch| touch.run.ref.id }
|
|
189
|
+
end
|
|
190
|
+
|
|
120
191
|
# Every paired run's touches, with what +view+ sends of its output.
|
|
121
192
|
def all_touches(conversation, cwd, view)
|
|
122
193
|
stored = LLMContextStale.paired_runs(conversation)
|
|
@@ -159,6 +230,9 @@ module Samagotchi
|
|
|
159
230
|
ToolResponse.interrupted_text?(text) || text.sub(ToolResponse::LEAD, "").lstrip.start_with?("Error")
|
|
160
231
|
end
|
|
161
232
|
|
|
233
|
+
# The other names +touch+'s output holds (a memory_read of several).
|
|
234
|
+
def shared_with(touch, key) = touch.share > 1 ? found_names(touch.run) - [key] : []
|
|
235
|
+
|
|
162
236
|
# A memory_read's names (Tools::MemoryRead.parse_names: none for the
|
|
163
237
|
# index listing), less the ones its output says weren't found.
|
|
164
238
|
def found_names(run)
|
|
@@ -168,7 +242,7 @@ module Samagotchi
|
|
|
168
242
|
names - missing
|
|
169
243
|
end
|
|
170
244
|
|
|
171
|
-
def item(kind, key, group, conversation, view, stat)
|
|
245
|
+
def item(kind, key, group, conversation, view, stat, pinned)
|
|
172
246
|
first = group.first
|
|
173
247
|
contents = group.select(&:content)
|
|
174
248
|
tokens = contents.sum { |touch| touch.sent.length / TokenUsage::CHARS_PER_TOKEN / touch.share }.ceil
|
|
@@ -180,7 +254,7 @@ module Samagotchi
|
|
|
180
254
|
LIVE
|
|
181
255
|
end
|
|
182
256
|
Item.new(kind: kind, key: key, state: state, tokens: tokens, ops: group.map(&:op).uniq, count: group.size,
|
|
183
|
-
first_id: first.run.ref.id)
|
|
257
|
+
first_id: first.run.ref.id, pinned: pinned, has_drop: contents.any? { |touch| dropped?(touch, conversation) })
|
|
184
258
|
end
|
|
185
259
|
|
|
186
260
|
# Whether the model is still sent +touch+'s output: whole (with the
|
|
@@ -233,6 +307,6 @@ module Samagotchi
|
|
|
233
307
|
|
|
234
308
|
def field(hash, key) = hash.key?(key) ? hash[key] : hash[key.to_s]
|
|
235
309
|
|
|
236
|
-
private_class_method :all_touches, :touches, :failed?, :found_names, :item, :still_sent?, :dropped?, :with_id, :changed?, :stamp_of, :field
|
|
310
|
+
private_class_method :all_touches, :touches, :failed?, :found_names, :shared_with, :item, :still_sent?, :dropped?, :with_id, :changed?, :stamp_of, :field
|
|
237
311
|
end
|
|
238
312
|
end
|
|
@@ -2,22 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "working_set"
|
|
4
4
|
require_relative "working_set_steer"
|
|
5
|
+
require_relative "tools/memory"
|
|
5
6
|
|
|
6
7
|
module Samagotchi
|
|
7
8
|
# /working-set: with no arguments the session's working set (WorkingSet,
|
|
8
9
|
# as the web's list shows it: each item, what was done to it, its state
|
|
9
|
-
# and tokens, then the changes the next turn starts with); `drop KEY
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# and tokens, then the changes the next turn starts with); `drop KEY…`,
|
|
11
|
+
# `undrop KEY…`, `pin KEY…`, `unpin KEY…`, `add KEY…` and `unadd KEY…`
|
|
12
|
+
# steer it (WorkingSetSteer). An add asks the model, in the next turn's
|
|
13
|
+
# note, to read the item (a file that exists, a memory, an http(s) URL).
|
|
14
|
+
# KEY: a path (absolute or the session's folder's), memory:NAME, a URL;
|
|
15
|
+
# one with spaces quoted ("my dir/a.rb", WorkingSetSteer.words), as the
|
|
16
|
+
# listing shows it.
|
|
17
|
+
# A pin is saved on the session (Session#working_set_pins); the turn-start
|
|
18
|
+
# note names it only where the model has forget_outputs (the strategy's
|
|
19
|
+
# forget layer), or to cancel a pin or an unpin it named.
|
|
12
20
|
#
|
|
13
21
|
# An op runs between turns (mid_turn :queue: a line sent while a turn
|
|
14
22
|
# runs waits for its end), on a copy of the session's messages that
|
|
15
23
|
# replaces them, so the next turn's first request is the first with it.
|
|
16
24
|
class WorkingSetCommand
|
|
17
25
|
NAME = "/working-set"
|
|
18
|
-
USAGE = "usage: #{NAME} [drop KEY… | undrop KEY…
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
USAGE = "usage: #{NAME} [drop KEY… | undrop KEY… | pin KEY… | unpin KEY… | add KEY… | unadd KEY…] " \
|
|
27
|
+
"(KEY: a path, memory:NAME or a URL; \"quote\" one with spaces)".freeze
|
|
28
|
+
VERBS = [WorkingSetSteer::DROP, WorkingSetSteer::UNDROP, WorkingSetSteer::PIN, WorkingSetSteer::UNPIN,
|
|
29
|
+
WorkingSetSteer::ADD, WorkingSetSteer::UNADD].freeze
|
|
30
|
+
OP_WORDS = { WorkingSetSteer::DROP => "dropped", WorkingSetSteer::UNDROP => "undropped",
|
|
31
|
+
WorkingSetSteer::PIN => "pinned", WorkingSetSteer::UNPIN => "unpinned",
|
|
32
|
+
WorkingSetSteer::ADD => "added", WorkingSetSteer::UNADD => "unadded" }.freeze
|
|
33
|
+
PIN_OPS = [WorkingSetSteer::PIN, WorkingSetSteer::UNPIN].freeze
|
|
21
34
|
|
|
22
35
|
# @param engine [Engine] #session, #working_set_strategy,
|
|
23
36
|
# #clone_messages, #replace_messages
|
|
@@ -28,39 +41,39 @@ module Samagotchi
|
|
|
28
41
|
end
|
|
29
42
|
|
|
30
43
|
# @param args [String] what follows /working-set
|
|
31
|
-
# @return [Array(String,
|
|
32
|
-
#
|
|
44
|
+
# @return [Array(String, Array<Symbol>, Symbol)] the reply, what
|
|
45
|
+
# changed (:messages, :working_set_pins; none when nothing did), and
|
|
46
|
+
# :ok or :error (every key refused)
|
|
33
47
|
# @raise [ArgumentError] a line that isn't one (its message and USAGE)
|
|
34
48
|
def run(args)
|
|
35
|
-
verb, *words = args
|
|
36
|
-
return [listing,
|
|
49
|
+
verb, *words = WorkingSetSteer.words(args)
|
|
50
|
+
return [listing, [], :ok] if verb.nil?
|
|
37
51
|
raise ArgumentError, USAGE unless VERBS.include?(verb) && !words.empty?
|
|
38
52
|
|
|
39
53
|
session = @engine.session or raise ArgumentError, "no session yet"
|
|
40
54
|
cwd = session.working_directory
|
|
41
55
|
strategy = @engine.working_set_strategy
|
|
42
56
|
messages = @engine.clone_messages(session.messages)
|
|
57
|
+
pins = session.working_set_pins
|
|
43
58
|
lines = []
|
|
44
59
|
done = 0
|
|
60
|
+
noted = false
|
|
45
61
|
words.each do |word|
|
|
46
62
|
target = WorkingSetSteer.target(word, cwd: cwd)
|
|
47
|
-
result =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
lines << done_line(result)
|
|
63
|
+
result = op(verb, messages, pins, target, strategy, cwd)
|
|
64
|
+
[result.undone, result].compact.select { |each| noted?(each, messages, strategy) }.each do |each|
|
|
65
|
+
messages = WorkingSetSteer.record!(messages, each)
|
|
66
|
+
noted = true
|
|
67
|
+
end
|
|
68
|
+
pins = WorkingSetSteer.pinning(pins, result) if PIN_OPS.include?(result.op)
|
|
69
|
+
lines << done_line(result.undone) if result.undone
|
|
70
|
+
lines << done_line(result, strategy)
|
|
55
71
|
done += 1
|
|
56
72
|
rescue WorkingSetSteer::Refused => e
|
|
57
|
-
lines << "#{verb} #{word}: #{e.message}"
|
|
73
|
+
lines << "#{verb} #{WorkingSetSteer.quote(word)}: #{e.message}"
|
|
58
74
|
end
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@save.call(session)
|
|
62
|
-
end
|
|
63
|
-
[lines.join("\n"), done.positive?, done.positive? ? :ok : :error]
|
|
75
|
+
changed = done.positive? ? save(session, noted && messages, pins) : []
|
|
76
|
+
[lines.join("\n"), changed, done.positive? ? :ok : :error]
|
|
64
77
|
end
|
|
65
78
|
|
|
66
79
|
# The set under the strategy the next turn runs, and the pending ops.
|
|
@@ -69,10 +82,11 @@ module Samagotchi
|
|
|
69
82
|
return "no session yet" unless session
|
|
70
83
|
|
|
71
84
|
messages = Array(session.messages)
|
|
72
|
-
items = WorkingSet.build(messages, cwd: session.working_directory, strategy: @engine.working_set_strategy
|
|
85
|
+
items = WorkingSet.build(messages, cwd: session.working_directory, strategy: @engine.working_set_strategy,
|
|
86
|
+
pins: session.working_set_pins).items
|
|
73
87
|
return "working set: empty (the files, memories and URLs the turns read show here)\n#{USAGE}" if items.empty?
|
|
74
88
|
|
|
75
|
-
rows = items.map { |item| [name(item, session.working_directory), words(item)] }
|
|
89
|
+
rows = items.map { |item| [WorkingSetSteer.quote(name(item, session.working_directory)), words(item)] }
|
|
76
90
|
width = rows.map { |row, _| row.length }.max
|
|
77
91
|
lines = ["working set (#{items.size} item#{"s" unless items.size == 1}):"]
|
|
78
92
|
lines.concat(rows.map { |row, text| " #{row.ljust(width)} #{text}" })
|
|
@@ -86,6 +100,65 @@ module Samagotchi
|
|
|
86
100
|
|
|
87
101
|
private
|
|
88
102
|
|
|
103
|
+
def op(verb, messages, pins, target, strategy, cwd)
|
|
104
|
+
case verb
|
|
105
|
+
when WorkingSetSteer::DROP then WorkingSetSteer.drop(messages, target, strategy: strategy, cwd: cwd, pins: pins)
|
|
106
|
+
when WorkingSetSteer::UNDROP
|
|
107
|
+
sent = !WorkingSetSteer.pending?(messages, WorkingSetSteer::DROP, target)
|
|
108
|
+
WorkingSetSteer.undrop(messages, target, strategy: strategy, cwd: cwd, sent: sent)
|
|
109
|
+
when WorkingSetSteer::PIN then WorkingSetSteer.pin(messages, pins, target, strategy: strategy, cwd: cwd)
|
|
110
|
+
when WorkingSetSteer::UNPIN then WorkingSetSteer.unpin(pins, target)
|
|
111
|
+
when WorkingSetSteer::ADD
|
|
112
|
+
WorkingSetSteer.add(messages, target, strategy: strategy, cwd: cwd, found: method(:missing))
|
|
113
|
+
else WorkingSetSteer.unadd(messages, target)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Why an added +target+ can't be read (a Refused reason), nil when it
|
|
118
|
+
# can: a file that is there, a memory the project or system scope
|
|
119
|
+
# holds (memory_read's look). A URL is fetched as it is.
|
|
120
|
+
def missing(target)
|
|
121
|
+
case target.kind
|
|
122
|
+
when WorkingSet::FILE
|
|
123
|
+
return :directory if File.directory?(target.key)
|
|
124
|
+
|
|
125
|
+
File.file?(target.key) ? nil : :no_file
|
|
126
|
+
when WorkingSet::MEMORY
|
|
127
|
+
return :bad_memory if target.key.empty? || Tools::MemoryRead.invalid_name?(target.key)
|
|
128
|
+
|
|
129
|
+
found = %w[project system].any? { |scope| File.file?(Tools::MemoryRead.memory_path(target.key, scope)) }
|
|
130
|
+
found ? nil : :no_memory
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Whether the turn-start note lists +done+: a drop or an undrop always;
|
|
135
|
+
# a pin or an unpin where the model has forget_outputs, or when it
|
|
136
|
+
# cancels one the note lists.
|
|
137
|
+
def noted?(done, messages, strategy)
|
|
138
|
+
return true unless PIN_OPS.include?(done.op)
|
|
139
|
+
|
|
140
|
+
forget?(strategy) || WorkingSetSteer.pending?(messages, WorkingSetSteer::INVERSE.fetch(done.op), done.target)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def forget?(strategy) = Array(strategy).include?(:forget)
|
|
144
|
+
|
|
145
|
+
# Puts the ops' messages (nil: unchanged) and pins in place and saves
|
|
146
|
+
# the session.
|
|
147
|
+
# @return [Array<Symbol>] what changed
|
|
148
|
+
def save(session, messages, pins)
|
|
149
|
+
changed = []
|
|
150
|
+
if messages
|
|
151
|
+
@engine.replace_messages(messages)
|
|
152
|
+
changed << :messages
|
|
153
|
+
end
|
|
154
|
+
unless pins == session.working_set_pins
|
|
155
|
+
session.working_set_pins = pins
|
|
156
|
+
changed << :working_set_pins
|
|
157
|
+
end
|
|
158
|
+
@save.call(session) unless changed.empty?
|
|
159
|
+
changed
|
|
160
|
+
end
|
|
161
|
+
|
|
89
162
|
def name(item, cwd)
|
|
90
163
|
case item.kind
|
|
91
164
|
when WorkingSet::FILE then WorkingSetSteer.target(item.key, cwd: cwd).label
|
|
@@ -98,17 +171,47 @@ module Samagotchi
|
|
|
98
171
|
parts = [item.ops.join(", ")]
|
|
99
172
|
parts << "×#{item.count}" if item.count > 1
|
|
100
173
|
parts << item.state unless item.state == WorkingSet::LIVE
|
|
174
|
+
# A drop on some of its reads (the rest forgotten or still sent): undrop takes it back.
|
|
175
|
+
parts << "part dropped" if item.has_drop && item.state != WorkingSet::DROPPED
|
|
176
|
+
parts << "pinned" if item.pinned
|
|
101
177
|
parts << "~#{item.tokens} tokens" if item.tokens.positive?
|
|
102
178
|
parts.join(" · ")
|
|
103
179
|
end
|
|
104
180
|
|
|
105
|
-
def done_line(done)
|
|
106
|
-
|
|
107
|
-
|
|
181
|
+
def done_line(done, strategy = nil)
|
|
182
|
+
ids = done.ids.empty? ? "" : " (#{done.ids.join(", ")})"
|
|
183
|
+
"#{op_line(done, "#{OP_WORDS[done.op]} #{done.target.label}#{ids}", strategy)}#{with_line(done)}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# A memory_read of several names is one output: the op acted on the
|
|
187
|
+
# others' read too. Says which names went with it.
|
|
188
|
+
def with_line(done)
|
|
189
|
+
return "" if done.shared_with.empty?
|
|
190
|
+
|
|
191
|
+
names = done.shared_with.map { |name| "memory #{name}" }.join(", ")
|
|
192
|
+
"; the same memory_read holds #{names}: #{OP_WORDS[done.op]} with it (that read only)"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def op_line(done, what, strategy)
|
|
196
|
+
case done.op
|
|
197
|
+
when WorkingSetSteer::PIN
|
|
198
|
+
off = forget?(strategy) ? "" : " (the strategy has no forget layer now: the pin holds when it has)"
|
|
199
|
+
"#{what}: forget_outputs refuses its outputs, its later reads too#{off}"
|
|
200
|
+
when WorkingSetSteer::UNPIN then "#{what}: the model may forget it again"
|
|
201
|
+
when WorkingSetSteer::ADD
|
|
202
|
+
"#{what}: the next turn's note asks the model to read it first (one tool call)"
|
|
203
|
+
when WorkingSetSteer::UNADD then "#{what}: its add was never sent; nothing changes"
|
|
204
|
+
when WorkingSetSteer::DROP
|
|
108
205
|
frees = done.freed_chars.positive? ? "frees #{WorkingSetSteer.k(done.freed_chars)}" : "frees nothing, the stubs are as long"
|
|
109
206
|
"#{what}: the next turn's first request sends the stubs (#{frees}; the server reads the " \
|
|
110
207
|
"#{WorkingSetSteer.k(done.tail_chars)} after them again)"
|
|
111
|
-
|
|
208
|
+
else
|
|
209
|
+
undrop_line(what, done)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def undrop_line(what, done)
|
|
214
|
+
if done.free
|
|
112
215
|
"#{what}: its drop was never sent; nothing changes"
|
|
113
216
|
else
|
|
114
217
|
"#{what}: the next request sends it whole again (the server reads the #{WorkingSetSteer.k(done.tail_chars)} " \
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# One item of the working set the user pinned (/working-set pin): its
|
|
5
|
+
# identity as WorkingSet names an item, +kind+ file|memory|url and +key+
|
|
6
|
+
# (a file's absolute path, a memory's name, a URL). The model's
|
|
7
|
+
# forget_outputs refuses its outputs, the ones it has and the ones it
|
|
8
|
+
# reads later (WorkingSetSteer). Saved as the session's
|
|
9
|
+
# working_set_pins ([{"kind", "key"}]); a respawn, a continue and a fork
|
|
10
|
+
# keep them (SessionSetup).
|
|
11
|
+
WorkingSetPin = Data.define(:kind, :key) do
|
|
12
|
+
def initialize(kind:, key:)
|
|
13
|
+
super(kind: kind.to_s, key: key.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The session file's form.
|
|
17
|
+
def to_file = { "kind" => kind, "key" => key }
|
|
18
|
+
|
|
19
|
+
# +value+'s pins, in order, no repeats: pins, or a file's Hashes
|
|
20
|
+
# (string or symbol keys); anything else (a hand-edited entry without
|
|
21
|
+
# a kind or key) is left out.
|
|
22
|
+
# @return [Array<WorkingSetPin>] frozen
|
|
23
|
+
def self.list(value)
|
|
24
|
+
Array(value).filter_map do |entry|
|
|
25
|
+
next entry if entry.is_a?(WorkingSetPin)
|
|
26
|
+
next unless entry.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
kind = entry["kind"] || entry[:kind]
|
|
29
|
+
key = entry["key"] || entry[:key]
|
|
30
|
+
new(kind: kind, key: key) unless kind.to_s.empty? || key.to_s.empty?
|
|
31
|
+
end.uniq.freeze
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|