samagotchi 0.170.0 → 0.171.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/architecture.md +2 -2
- data/docs/cli.md +37 -23
- data/docs/configuration.md +1 -1
- data/docs/internals/block-renderers.md +2 -2
- data/docs/internals/bridge.md +5 -4
- data/docs/internals/context-telemetry.md +13 -1
- data/docs/internals/engine.md +4 -4
- data/docs/internals/inline-widgets.md +1 -1
- data/docs/internals/objectives.md +1 -1
- data/docs/internals/participants.md +235 -119
- data/docs/internals/web-frontend.md +164 -33
- data/docs/internals/working-set.md +69 -5
- data/docs/releasing.md +2 -1
- data/docs/sessions.md +6 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +8 -4
- data/lib/samagotchi/bridge.rb +39 -12
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/delivery.rb +13 -0
- data/lib/samagotchi/engine.rb +5 -1
- data/lib/samagotchi/event_fanout.rb +22 -1
- data/lib/samagotchi/grants.rb +5 -0
- data/lib/samagotchi/guardrails/review_paths.rb +2 -2
- data/lib/samagotchi/image_store.rb +51 -12
- data/lib/samagotchi/kernel_loop.rb +7 -1
- data/lib/samagotchi/llm/stray_tool_call.rb +7 -4
- data/lib/samagotchi/llm_context_stale.rb +4 -1
- data/lib/samagotchi/participant_messages.rb +39 -0
- data/lib/samagotchi/participant_revise.rb +61 -0
- data/lib/samagotchi/participant_uploads.rb +98 -0
- data/lib/samagotchi/reply_wait/poll.rb +5 -1
- data/lib/samagotchi/revise_anchor.rb +9 -0
- data/lib/samagotchi/send_command.rb +5 -0
- data/lib/samagotchi/session_inbox.rb +20 -12
- data/lib/samagotchi/session_manager/delivery.rb +7 -0
- data/lib/samagotchi/session_stores.rb +2 -0
- data/lib/samagotchi/tool_runner.rb +39 -5
- data/lib/samagotchi/tools/memory.rb +26 -7
- data/lib/samagotchi/turn_anchors.rb +0 -3
- data/lib/samagotchi/turn_stages/verify_anchors.rb +13 -1
- data/lib/samagotchi/verify_anchor.rb +6 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +42 -77
- data/lib/samagotchi/web/participant_access.rb +129 -26
- data/lib/samagotchi/web/participant_images.rb +185 -0
- data/lib/samagotchi/web/participant_review.rb +19 -138
- data/lib/samagotchi/web/participant_show.rb +240 -0
- data/lib/samagotchi/web/participant_stream_filter.rb +25 -2
- data/lib/samagotchi/web/participant_view.rb +331 -24
- data/lib/samagotchi/web/public/annotate_bar.css +1 -2
- data/lib/samagotchi/web/public/annotate_bar.js +1 -2
- data/lib/samagotchi/web/public/app.js +124 -88
- data/lib/samagotchi/web/public/block_host.js +7 -4
- data/lib/samagotchi/web/public/bubbles.js +9 -1
- data/lib/samagotchi/web/public/cards_offscreen.js +69 -0
- data/lib/samagotchi/web/public/chip_popover.js +5 -4
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer.css +4 -6
- data/lib/samagotchi/web/public/composer_mode.js +11 -6
- data/lib/samagotchi/web/public/composer_mount.js +4 -4
- data/lib/samagotchi/web/public/ctx.js +12 -3
- data/lib/samagotchi/web/public/delivery_line.js +34 -5
- data/lib/samagotchi/web/public/history_shape.js +5 -1
- data/lib/samagotchi/web/public/images.js +7 -1
- data/lib/samagotchi/web/public/index.html +40 -4
- data/lib/samagotchi/web/public/info_bar_model.js +2 -1
- data/lib/samagotchi/web/public/markdown_place.js +5 -4
- data/lib/samagotchi/web/public/owner_claim.js +3 -3
- data/lib/samagotchi/web/public/owner_verify.js +0 -2
- data/lib/samagotchi/web/public/participant_bar.js +108 -0
- data/lib/samagotchi/web/public/participant_main.js +289 -0
- data/lib/samagotchi/web/public/participant_model.js +5 -292
- data/lib/samagotchi/web/public/participant_names.js +6 -1
- data/lib/samagotchi/web/public/participant_revise.js +66 -0
- data/lib/samagotchi/web/public/participants_panel.js +3 -12
- data/lib/samagotchi/web/public/question_cards.js +14 -3
- data/lib/samagotchi/web/public/refs.js +53 -36
- data/lib/samagotchi/web/public/reload_check.js +68 -11
- data/lib/samagotchi/web/public/revise_stack.js +21 -10
- data/lib/samagotchi/web/public/send_turn.js +24 -6
- data/lib/samagotchi/web/public/session_fields.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +12 -4
- data/lib/samagotchi/web/public/session_sync.js +9 -2
- data/lib/samagotchi/web/public/session_view.js +17 -7
- data/lib/samagotchi/web/public/stage_asking.js +6 -4
- data/lib/samagotchi/web/public/stage_owner_wait.js +35 -0
- data/lib/samagotchi/web/public/stage_view.js +18 -4
- data/lib/samagotchi/web/public/stage_waiting.js +74 -6
- data/lib/samagotchi/web/public/storage.js +22 -0
- data/lib/samagotchi/web/public/stream_handlers.js +2 -1
- data/lib/samagotchi/web/public/tokens.css +1 -1
- data/lib/samagotchi/web/public/turn_block.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +15 -6
- data/lib/samagotchi/web/public/turn_html.js +9 -2
- data/lib/samagotchi/web/public/turn_range.js +1 -2
- data/lib/samagotchi/web/public/verify_target.js +2 -4
- data/lib/samagotchi/web/public/viewer.js +59 -0
- data/lib/samagotchi/web/public/viewer_self.js +53 -0
- data/lib/samagotchi/web/public/working_set.js +11 -5
- data/lib/samagotchi/web/public/working_set_view.js +53 -8
- data/lib/samagotchi/web/public/ws_cards.js +89 -0
- data/lib/samagotchi/web/public/ws_peek.js +106 -0
- data/lib/samagotchi/web/public/ws_preview.css +126 -0
- data/lib/samagotchi/web/public/ws_reader.js +319 -0
- data/lib/samagotchi/web/public/ws_timeline.js +200 -0
- data/lib/samagotchi/web/public/ws_way_in.js +25 -0
- data/lib/samagotchi/web/session_control.rb +39 -14
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +75 -28
- data/lib/samagotchi/web/viewer.rb +70 -0
- data/lib/samagotchi/web/working_set_routes.rb +132 -0
- data/lib/samagotchi/worker.rb +19 -14
- data/lib/samagotchi/working_set.rb +260 -8
- metadata +20 -6
- data/lib/samagotchi/web/public/participant.html +0 -218
- data/lib/samagotchi/web/public/participant.js +0 -719
- data/lib/samagotchi/web/public/participant_annotate.js +0 -76
- data/lib/samagotchi/web/public/review_history.js +0 -72
- data/lib/samagotchi/web/public/tools_overview.js +0 -42
|
@@ -7,6 +7,8 @@ require_relative "../participant_mode"
|
|
|
7
7
|
require_relative "../revise_anchor"
|
|
8
8
|
require_relative "../verify_anchor"
|
|
9
9
|
require_relative "../redact"
|
|
10
|
+
require_relative "../file_ref"
|
|
11
|
+
require_relative "../image_store"
|
|
10
12
|
|
|
11
13
|
module Samagotchi
|
|
12
14
|
module Web
|
|
@@ -16,7 +18,10 @@ module Samagotchi
|
|
|
16
18
|
# waiting states. Verdict cards expose only their anchor, verdict,
|
|
17
19
|
# reason and author. Never: tool output or arguments, thinking, context
|
|
18
20
|
# notes, plugin steers, automatic input, other cards, commands,
|
|
19
|
-
# questions' internals, memories, the working set
|
|
21
|
+
# questions' internals, memories, the working set's memories and URLs,
|
|
22
|
+
# cost, tokens per model, host paths. Numbers they read: the context's
|
|
23
|
+
# fill, the speed, the turn times (#context_status, #generation_completed,
|
|
24
|
+
# #timing).
|
|
20
25
|
#
|
|
21
26
|
# Every read a participant reaches goes through here: the stream's
|
|
22
27
|
# events (#event, ParticipantStreamFilter), the review route's
|
|
@@ -42,20 +47,28 @@ module Samagotchi
|
|
|
42
47
|
# fields only (#verdict_record).
|
|
43
48
|
"verdict_recorded" => :verdict_recorded,
|
|
44
49
|
"generation_chunk" => :generation_chunk,
|
|
50
|
+
# A generation's end: its decode speed only (the composer's tok/s).
|
|
51
|
+
"generation_completed" => :generation_completed,
|
|
52
|
+
# The context's fill (the composer's ctx %): its numbers only.
|
|
53
|
+
"context_status" => :context_status,
|
|
54
|
+
# The files chi works with (the stage's working set): the project's
|
|
55
|
+
# files only (#working_set).
|
|
56
|
+
"working_set_updated" => :working_set_updated,
|
|
45
57
|
"generation_retrying" => :generation_retrying,
|
|
46
58
|
"tool_call_started" => :tool_call_started,
|
|
47
59
|
"tool_call_completed" => :tool_call_completed,
|
|
48
|
-
# Queue changes: the
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
60
|
+
# Queue changes: the viewer's own in the owner's shape (the main
|
|
61
|
+
# page matches its bubbles by them), anyone else's bare (their
|
|
62
|
+
# prompts are theirs).
|
|
63
|
+
"turn_enqueued" => :turn_enqueued,
|
|
64
|
+
"turn_dequeued" => :turn_dequeued,
|
|
65
|
+
"input_merged" => :input_merged,
|
|
53
66
|
# The owner's question, approval or Continue offer: "waiting for the
|
|
54
|
-
# owner", nothing of it.
|
|
55
|
-
"question_requested" => :
|
|
67
|
+
# owner" and for what kind of thing (#waiting_kind), nothing of it.
|
|
68
|
+
"question_requested" => :question_requested,
|
|
56
69
|
"question_answered" => :bare,
|
|
57
70
|
"question_cancelled" => :bare,
|
|
58
|
-
"continue_offered" => :
|
|
71
|
+
"continue_offered" => :continue_offered,
|
|
59
72
|
"continue_resolved" => :bare,
|
|
60
73
|
# The cursor can't be replayed (or a join): the page re-reads.
|
|
61
74
|
"snapshot" => :bare,
|
|
@@ -68,12 +81,12 @@ module Samagotchi
|
|
|
68
81
|
# named is dropped all the same.
|
|
69
82
|
DROPPED = %w[
|
|
70
83
|
after_generation after_tool_call after_turn answer_display before_generation before_tool_call before_turn
|
|
71
|
-
card command_output command_queued command_ran context_added
|
|
72
|
-
generation_cancelled
|
|
84
|
+
card command_output command_queued command_ran context_added empty_answer_retry
|
|
85
|
+
generation_cancelled generation_progress generation_started guardrail_warning hook_notice
|
|
73
86
|
llm_context_edited merged_input objectives_updated participants_changed pending_input_merged plugin_init_finished
|
|
74
87
|
plugin_init_started plugin_init_wait prompt_restored question question_relay reasoning_history_refused
|
|
75
88
|
recap_ready reminder_injected sampling_refused session_end session_start side_usage steer_cut thinking_dropped
|
|
76
|
-
thinking_refused tool_dispatch_completed tool_dispatch_started used_memories_updated
|
|
89
|
+
thinking_refused tool_dispatch_completed tool_dispatch_started used_memories_updated
|
|
77
90
|
].freeze
|
|
78
91
|
|
|
79
92
|
# A merged input line's saved sources a participant sees: a person's
|
|
@@ -88,16 +101,53 @@ module Samagotchi
|
|
|
88
101
|
|
|
89
102
|
# @param event [Hash] a client event (ClientEvent's shape; string keys
|
|
90
103
|
# from the stream's JSON)
|
|
104
|
+
# @param own_enqueued [#include?] the enqueued ids of the viewer's own
|
|
105
|
+
# turns (a :turn_dequeued names no author; ParticipantStreamFilter's)
|
|
91
106
|
# @return [Hash, nil] what a participant gets, string keys; nil: nothing
|
|
92
|
-
def event(event, viewer)
|
|
107
|
+
def event(event, viewer, own_enqueued: [])
|
|
93
108
|
return nil unless event.is_a?(Hash)
|
|
94
109
|
|
|
95
110
|
type = field(event, :type).to_s
|
|
96
111
|
method = EVENTS[type] or return nil
|
|
97
|
-
out = send(method, event, viewer)
|
|
112
|
+
out = method == :turn_dequeued ? turn_dequeued(event, viewer, own_enqueued) : send(method, event, viewer)
|
|
98
113
|
out && Redact.deep(out, viewer)
|
|
99
114
|
end
|
|
100
115
|
|
|
116
|
+
# A turn queued: the viewer's own with its enqueued id, client id,
|
|
117
|
+
# prompt, delivery, author and image refs; anyone else's bare.
|
|
118
|
+
def turn_enqueued(event, viewer)
|
|
119
|
+
entry = queued_entry(event, viewer) or return bare(event, viewer)
|
|
120
|
+
|
|
121
|
+
{ "type" => "turn_enqueued", **seq(event), **entry }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A queued turn taken back or refused: the viewer's own with its
|
|
125
|
+
# enqueued id (never the client that took it back); anyone else's bare.
|
|
126
|
+
def turn_dequeued(event, viewer, own_enqueued)
|
|
127
|
+
enqueued_id = field(event, :enqueued_id)
|
|
128
|
+
return bare(event, viewer) unless enqueued_id.is_a?(String) && !enqueued_id.empty? && own_enqueued.include?(enqueued_id)
|
|
129
|
+
|
|
130
|
+
{ "type" => "turn_dequeued", **seq(event), "enqueued_id" => enqueued_id }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Input merged into the running turn: the viewer's own lines (their
|
|
134
|
+
# origins' enqueued ids) and how many, bare when none is theirs. The
|
|
135
|
+
# others' (a delegate's report, another person's line) are not named
|
|
136
|
+
# nor counted. No text: it comes with the turn's messages.
|
|
137
|
+
def input_merged(event, viewer)
|
|
138
|
+
own = Array(field(event, :origins)).filter_map do |origin|
|
|
139
|
+
next unless origin.is_a?(Hash) && viewer.own?(field(origin, :author))
|
|
140
|
+
|
|
141
|
+
enqueued_id = field(origin, :enqueued_id)
|
|
142
|
+
next unless enqueued_id.is_a?(String) && !enqueued_id.empty?
|
|
143
|
+
|
|
144
|
+
{ "enqueued_id" => enqueued_id, "client_id" => field(origin, :client_id).to_s, "author" => field(origin, :author).to_s }
|
|
145
|
+
end
|
|
146
|
+
return bare(event, viewer) if own.empty?
|
|
147
|
+
|
|
148
|
+
{ "type" => "input_merged", **seq(event), "count" => own.size, "origins" => own }
|
|
149
|
+
end
|
|
150
|
+
|
|
101
151
|
# A turn's start: who wrote it, and its prompt when a person sent it
|
|
102
152
|
# (an automatic turn's prompt, a reminder's or a task's, is not
|
|
103
153
|
# shown: a person is the owner's web or TUI, chi send, a participant,
|
|
@@ -108,6 +158,8 @@ module Samagotchi
|
|
|
108
158
|
author = field(event, :author) || origin.author
|
|
109
159
|
out = { "type" => "turn_started", **id_fields(event), **time_field(event) }
|
|
110
160
|
out.merge!(author_fields(author, field(event, :author_name), viewer, field(event, :participant_mode)))
|
|
161
|
+
images = image_refs(field(event, :images), author, viewer)
|
|
162
|
+
out["images"] = images if images
|
|
111
163
|
prompt = field(event, :prompt)
|
|
112
164
|
if field(event, :continue)
|
|
113
165
|
out["continue"] = true
|
|
@@ -119,14 +171,30 @@ module Samagotchi
|
|
|
119
171
|
out
|
|
120
172
|
end
|
|
121
173
|
|
|
174
|
+
# A drawing's filtered turn_started (#event's) with +prompt+, the
|
|
175
|
+
# filtered messages' prompt of its turn; nil: an automatic turn's
|
|
176
|
+
# start, no prompt (a Continue's has none either way). Any other event
|
|
177
|
+
# as it is.
|
|
178
|
+
def drawing_start(event, prompt)
|
|
179
|
+
return event unless event["type"] == "turn_started"
|
|
180
|
+
|
|
181
|
+
out = event.except("prompt", "automatic")
|
|
182
|
+
return out if out["continue"]
|
|
183
|
+
return out.merge("prompt" => prompt) if prompt
|
|
184
|
+
|
|
185
|
+
out.except("continue").merge("automatic" => true)
|
|
186
|
+
end
|
|
187
|
+
|
|
122
188
|
def turn_ended(event, viewer)
|
|
123
189
|
out = { "type" => field(event, :type).to_s, **id_fields(event) }
|
|
124
190
|
verdict = verdict_record(field(event, :verdict), viewer)
|
|
125
191
|
out["verdict"] = verdict if verdict
|
|
126
|
-
# The turn a completed Revise turn revised (as on its prompt
|
|
127
|
-
#
|
|
192
|
+
# The turn a completed Revise turn revised (as on its prompt), and
|
|
193
|
+
# whether it was a Verify turn (no Revise on its answer): a boolean,
|
|
194
|
+
# never the quote.
|
|
128
195
|
revises = ReviseAnchor.from(field(event, :revises))
|
|
129
196
|
out["revises"] = revises.to_file if revises
|
|
197
|
+
out["verify"] = true if field(event, :verify) == true
|
|
130
198
|
out
|
|
131
199
|
end
|
|
132
200
|
|
|
@@ -172,6 +240,82 @@ module Samagotchi
|
|
|
172
240
|
{ "type" => "generation_chunk", "text" => text, **iteration(event) }
|
|
173
241
|
end
|
|
174
242
|
|
|
243
|
+
# A generation's end: its decode speed ({decode_tps, source}, the
|
|
244
|
+
# composer's tok/s). Never the session's tokens, cost or models
|
|
245
|
+
# (+tokens+, +model_segments+), nor the served model.
|
|
246
|
+
def generation_completed(event, _viewer)
|
|
247
|
+
out = { "type" => "generation_completed", **iteration(event) }
|
|
248
|
+
speed = field(event, :speed)
|
|
249
|
+
tps = speed.is_a?(Hash) ? field(speed, :decode_tps) : nil
|
|
250
|
+
if tps.is_a?(Numeric) && tps.to_f.finite?
|
|
251
|
+
source = field(speed, :source).to_s
|
|
252
|
+
out["speed"] = { "decode_tps" => tps, **(SPEED_SOURCES.include?(source) ? { "source" => source } : {}) }
|
|
253
|
+
end
|
|
254
|
+
out
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
SPEED_SOURCES = %w[server estimate].freeze
|
|
258
|
+
|
|
259
|
+
# The usage numbers of a context_status a participant reads: its fill
|
|
260
|
+
# and the token counts.
|
|
261
|
+
CONTEXT_USAGE_KEYS = %w[estimated_pct estimated_used_tokens estimated_remaining_tokens window_tokens].freeze
|
|
262
|
+
|
|
263
|
+
# The context's fill (ContextStatus#observe, Engine's switched
|
|
264
|
+
# context): usage's percentage and token counts (usage null: unknown
|
|
265
|
+
# after a model switch), the bucket, and the window after a switch.
|
|
266
|
+
# Never +status+ (the model-facing line, with its guidance), the
|
|
267
|
+
# counts' source or the window's.
|
|
268
|
+
def context_status(event, _viewer)
|
|
269
|
+
out = { "type" => "context_status", **iteration(event) }
|
|
270
|
+
if event.key?("usage") || event.key?(:usage)
|
|
271
|
+
usage = field(event, :usage)
|
|
272
|
+
out["usage"] = usage.is_a?(Hash) ? pick(usage, CONTEXT_USAGE_KEYS) : nil
|
|
273
|
+
end
|
|
274
|
+
bucket = field(event, :bucket)
|
|
275
|
+
out["bucket"] = bucket if bucket.is_a?(String) && bucket.match?(/\A(?:under|\d+plus)\d*\z/)
|
|
276
|
+
if event.key?("context_window_tokens") || event.key?(:context_window_tokens)
|
|
277
|
+
window = field(event, :context_window_tokens)
|
|
278
|
+
out["context_window_tokens"] = window.is_a?(Integer) ? window : nil
|
|
279
|
+
end
|
|
280
|
+
out
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def working_set_updated(event, viewer)
|
|
284
|
+
{ "type" => "working_set_updated", **seq(event), **working_set(event, viewer) }
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# The working set's states and ops a participant's item may carry
|
|
288
|
+
# (WorkingSet::Item).
|
|
289
|
+
WORKING_SET_STATES = %w[live stale forgotten dropped].freeze
|
|
290
|
+
WORKING_SET_OPS = %w[read edit].freeze
|
|
291
|
+
|
|
292
|
+
# A working set (WorkingSet::Snapshot#to_h: the event's and GET
|
|
293
|
+
# …/working_set's) as a participant may read it: the file items inside
|
|
294
|
+
# the project, each key made project-relative, with its state, tokens,
|
|
295
|
+
# ops, count and pinned; +at+ (the page keeps the newer). Never a
|
|
296
|
+
# memory or URL item, a path outside the project, +first_id+,
|
|
297
|
+
# +has_drop+ or the pending +adds+.
|
|
298
|
+
def working_set(set, viewer)
|
|
299
|
+
items = Array(field(set, :items)).filter_map { |item| working_set_item(item, viewer) }
|
|
300
|
+
at = field(set, :at)
|
|
301
|
+
{ "items" => items, **(at.is_a?(Integer) ? { "at" => at } : {}) }
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def working_set_item(item, viewer)
|
|
305
|
+
return nil unless item.is_a?(Hash) && field(item, :kind).to_s == FileRef::KIND
|
|
306
|
+
|
|
307
|
+
key = project_path(field(item, :key), viewer.cwd, home: viewer.home) or return nil
|
|
308
|
+
state = field(item, :state).to_s
|
|
309
|
+
out = { "kind" => FileRef::KIND, "key" => key, "state" => WORKING_SET_STATES.include?(state) ? state : "live",
|
|
310
|
+
"ops" => Array(field(item, :ops)).map(&:to_s).select { |op| WORKING_SET_OPS.include?(op) }.uniq,
|
|
311
|
+
"pinned" => field(item, :pinned) == true }
|
|
312
|
+
%i[tokens count].each do |name|
|
|
313
|
+
value = field(item, name)
|
|
314
|
+
out[name.to_s] = value if value.is_a?(Integer) && value >= 0
|
|
315
|
+
end
|
|
316
|
+
out
|
|
317
|
+
end
|
|
318
|
+
|
|
175
319
|
# A dropped stream asked again: its text so far is void when restarted.
|
|
176
320
|
def generation_retrying(event, _viewer)
|
|
177
321
|
out = { "type" => "generation_retrying", **iteration(event) }
|
|
@@ -183,14 +327,42 @@ module Samagotchi
|
|
|
183
327
|
{ "type" => "tool_call_started", **tool_row(field(event, :tool), field(event, :ref), viewer), **call_ids(event) }
|
|
184
328
|
end
|
|
185
329
|
|
|
330
|
+
# Its status word, flat and as the owner's +activity+ ({status} only:
|
|
331
|
+
# never its action or params) for the main page's row (activity.js).
|
|
186
332
|
def tool_call_completed(event, _viewer)
|
|
187
333
|
out = { "type" => "tool_call_completed", **call_ids(event) }
|
|
188
334
|
activity = field(event, :activity)
|
|
189
335
|
status = activity.is_a?(Hash) ? field(activity, :status).to_s : ""
|
|
190
|
-
out
|
|
336
|
+
out.merge!("status" => status, "activity" => { "status" => status }) if STATUSES.include?(status)
|
|
191
337
|
out
|
|
192
338
|
end
|
|
193
339
|
|
|
340
|
+
# A question for the owner: its kind (#waiting_kind) only.
|
|
341
|
+
def question_requested(event, _viewer)
|
|
342
|
+
{ "type" => "question_requested", **seq(event), "kind" => waiting_kind(field(event, :pending_question)) }
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# The owner's Continue offer: its kind only.
|
|
346
|
+
def continue_offered(event, _viewer)
|
|
347
|
+
{ "type" => "continue_offered", **seq(event), "kind" => WAITING_CONTINUE }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
WAITING_APPROVAL = "approval"
|
|
351
|
+
WAITING_QUESTION = "question"
|
|
352
|
+
WAITING_CONTINUE = "continue"
|
|
353
|
+
|
|
354
|
+
# What a turn waits for the owner on, derived from a pending question
|
|
355
|
+
# (PendingQuestion#to_h, a snapshot's): "approval" for a guardrail's
|
|
356
|
+
# approval or a relayed one, "continue" for the step-limit question,
|
|
357
|
+
# "question" for anything else (the model's own, a hook's, none).
|
|
358
|
+
def waiting_kind(pending)
|
|
359
|
+
return WAITING_QUESTION unless pending.is_a?(Hash)
|
|
360
|
+
return WAITING_APPROVAL if field(pending, :kind).to_s == "approval" || field(pending, :relay).is_a?(Hash)
|
|
361
|
+
return WAITING_CONTINUE if field(pending, :kind).to_s == "continue"
|
|
362
|
+
|
|
363
|
+
WAITING_QUESTION
|
|
364
|
+
end
|
|
365
|
+
|
|
194
366
|
# Its type alone (and the cursor it was sent at).
|
|
195
367
|
def bare(event, _viewer)
|
|
196
368
|
{ "type" => field(event, :type).to_s, **seq(event) }
|
|
@@ -242,14 +414,17 @@ module Samagotchi
|
|
|
242
414
|
out["content"] = field(message, :content).to_s
|
|
243
415
|
out["turn_id"] = turn_id.to_s if turn_id
|
|
244
416
|
out["merged"] = true if merged
|
|
245
|
-
# The turn it revised (a Revise stack
|
|
417
|
+
# The turn it revised (a Revise stack), and whether it was a Verify
|
|
418
|
+
# turn (VerifyAnchor.marks?, ParticipantRevise's test too): a
|
|
419
|
+
# boolean only, never the quote.
|
|
246
420
|
revises = ReviseAnchor.from(field(message, :revises))
|
|
247
421
|
out["revises"] = revises.to_file if revises && turn_id && !merged
|
|
422
|
+
out["verify"] = true if turn_id && !merged && VerifyAnchor.marks?(field(message, :verify))
|
|
248
423
|
out.merge!(author_fields(field(message, :author), field(message, :author_name), viewer,
|
|
249
424
|
field(message, :participant_mode)))
|
|
250
|
-
images = field(message, :images)
|
|
251
|
-
out["images"] = images
|
|
252
|
-
out
|
|
425
|
+
images = image_refs(field(message, :images), field(message, :author), viewer)
|
|
426
|
+
out["images"] = images if images
|
|
427
|
+
out.merge!(drawn_field(message))
|
|
253
428
|
end
|
|
254
429
|
|
|
255
430
|
# A wake turn's start (its prompt a delegate's report or a context
|
|
@@ -275,9 +450,33 @@ module Samagotchi
|
|
|
275
450
|
|
|
276
451
|
out = { "role" => "assistant", "content" => content, **time_field(message) }
|
|
277
452
|
out["tools"] = rows unless rows.empty?
|
|
453
|
+
out.merge!(drawn_field(message))
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# The turn whose drawing draws this message (MessageDisplay's +drawn+
|
|
457
|
+
# stamp: a turn id), for the main page's show; none on the review read.
|
|
458
|
+
def drawn_field(message)
|
|
459
|
+
drawn = field(message, :drawn)
|
|
460
|
+
drawn.is_a?(String) && !drawn.empty? ? { "drawn" => drawn } : {}
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# A finished turn's drawing end (Bridge::TurnDrawings' end, its
|
|
464
|
+
# END_KEYS) as a participant may read it: how it ended, its turn and
|
|
465
|
+
# duration, and how many steps a failure kept. Never a failure's
|
|
466
|
+
# summary, message or class (they may name a host path), a cancel's
|
|
467
|
+
# reason or who stopped it.
|
|
468
|
+
def drawing_end(raw, turn_id)
|
|
469
|
+
out = { "type" => (TURN_ENDS.include?(field(raw, :type).to_s) ? field(raw, :type).to_s : "turn_completed"),
|
|
470
|
+
"turn_id" => turn_id.to_s }
|
|
471
|
+
duration = field(raw, :duration_ms)
|
|
472
|
+
out["duration_ms"] = duration if duration.is_a?(Integer)
|
|
473
|
+
kept = field(raw, :kept_steps)
|
|
474
|
+
out["kept_steps"] = kept if kept.is_a?(Integer)
|
|
278
475
|
out
|
|
279
476
|
end
|
|
280
477
|
|
|
478
|
+
TURN_ENDS = %w[turn_completed turn_canceled turn_failed].freeze
|
|
479
|
+
|
|
281
480
|
# The turn in progress (TurnAccumulator#current_turn, string keys
|
|
282
481
|
# from the Bridge's snapshot) as a participant sees it: its start
|
|
283
482
|
# (#turn_started's fields), the reply's text so far, its tool rows,
|
|
@@ -303,18 +502,126 @@ module Samagotchi
|
|
|
303
502
|
end
|
|
304
503
|
end
|
|
305
504
|
out = started.merge("text" => text, "tools" => tools)
|
|
306
|
-
|
|
505
|
+
# The running generation's phase (Bridge#with_generation: the main
|
|
506
|
+
# page's delivery-line clock): whether it only thinks so far, and its
|
|
507
|
+
# age; nothing of what it thinks.
|
|
508
|
+
generation = field(turn, :generation)
|
|
509
|
+
if generation.is_a?(Hash)
|
|
510
|
+
gen = {}
|
|
511
|
+
gen["thinking_only"] = field(generation, :thinking_only) if [true, false].include?(field(generation, :thinking_only))
|
|
512
|
+
gen["age"] = field(generation, :age) if field(generation, :age).is_a?(Numeric)
|
|
513
|
+
out["generation"] = gen
|
|
514
|
+
end
|
|
515
|
+
pending = field(turn, :pending_question)
|
|
516
|
+
out.merge!("waiting_for_owner" => true, "kind" => waiting_kind(pending)) if pending
|
|
307
517
|
Redact.deep(out, viewer)
|
|
308
518
|
end
|
|
309
519
|
|
|
520
|
+
# A queued entry (a Bridge snapshot's +queued+ row) when it is the
|
|
521
|
+
# viewer's own, in the owner's shape: its enqueued id, client id,
|
|
522
|
+
# prompt, delivery, author and image refs (#image_refs). nil for
|
|
523
|
+
# another person's (their prompt).
|
|
524
|
+
def queued_entry(entry, viewer)
|
|
525
|
+
return nil unless entry.is_a?(Hash) && viewer.own?(field(entry, :author))
|
|
526
|
+
|
|
527
|
+
out = { "enqueued_id" => field(entry, :enqueued_id).to_s, "client_id" => field(entry, :client_id).to_s,
|
|
528
|
+
"prompt" => field(entry, :prompt).to_s, "author" => field(entry, :author).to_s }
|
|
529
|
+
delivery = field(entry, :delivery).to_s
|
|
530
|
+
out["delivery"] = delivery if DELIVERIES.include?(delivery)
|
|
531
|
+
images = image_refs(field(entry, :images), field(entry, :author), viewer)
|
|
532
|
+
out["images"] = images if images
|
|
533
|
+
Redact.deep(out, viewer)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# A message's or a turn's images as a participant sees them: the
|
|
537
|
+
# viewer's own as refs ({file, name, width, height}: what the page's
|
|
538
|
+
# thumbnails load, the files they uploaded), only refs to a stored
|
|
539
|
+
# image (ImageStore::REF_RE); anyone else's as a count. Never source,
|
|
540
|
+
# bytes or mime. nil for none.
|
|
541
|
+
def image_refs(images, author, viewer)
|
|
542
|
+
return nil unless images.is_a?(Array) && !images.empty?
|
|
543
|
+
return images.size unless viewer.own?(author)
|
|
544
|
+
|
|
545
|
+
refs = images.filter_map do |ref|
|
|
546
|
+
file = field(ref, :file)
|
|
547
|
+
next unless file.is_a?(String) && ImageStore::REF_RE.match?(file)
|
|
548
|
+
|
|
549
|
+
out = { "file" => file, "name" => File.basename(field(ref, :name).to_s) }
|
|
550
|
+
%i[width height].each { |key| out[key.to_s] = field(ref, key) if field(ref, key).is_a?(Integer) }
|
|
551
|
+
out
|
|
552
|
+
end
|
|
553
|
+
refs.empty? ? nil : refs
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
DELIVERIES = %w[queue next_step].freeze
|
|
557
|
+
|
|
558
|
+
# A turn record's fields a participant reads: its id and times (the
|
|
559
|
+
# turn time) and its speed. Never its cost, tokens, model, author,
|
|
560
|
+
# cancel reason or failure.
|
|
561
|
+
TURN_RECORD_KEYS = %w[id started_at completed_at duration_ms decode_tps tps_source].freeze
|
|
562
|
+
# The running turn's (SessionMetrics#active_turn_snapshot).
|
|
563
|
+
ACTIVE_TURN_KEYS = %w[id started_at duration_ms].freeze
|
|
564
|
+
|
|
565
|
+
# The show's timing (SessionShow#timing_from) as a participant may
|
|
566
|
+
# read it: the session's clock, each turn's duration and speed, the
|
|
567
|
+
# running turn's start, and the context's fill (its token counts).
|
|
568
|
+
# Never tool_records (guardrail judgment ids, System 1 labels),
|
|
569
|
+
# active_tools, tokens, model_segments (cost per model), memory_index
|
|
570
|
+
# (memory names) or side (side requests' cost).
|
|
571
|
+
def timing(payload)
|
|
572
|
+
return nil unless payload.is_a?(Hash)
|
|
573
|
+
|
|
574
|
+
records = Array(field(payload, :turn_records)).filter_map { |record| pick(record, TURN_RECORD_KEYS) }
|
|
575
|
+
context = field(payload, :context)
|
|
576
|
+
{
|
|
577
|
+
"started_at" => string_or_nil(field(payload, :started_at)),
|
|
578
|
+
"last_activity_at" => string_or_nil(field(payload, :last_activity_at)),
|
|
579
|
+
"session_duration_ms" => number_or_nil(field(payload, :session_duration_ms)),
|
|
580
|
+
"turn_records" => records,
|
|
581
|
+
"tool_records" => [],
|
|
582
|
+
"turn_count" => number_or_nil(field(payload, :turn_count)),
|
|
583
|
+
"active_turn" => pick(field(payload, :active_turn), ACTIVE_TURN_KEYS),
|
|
584
|
+
"active_tools" => [],
|
|
585
|
+
"context" => pick(context, %w[used_tokens window_tokens]),
|
|
586
|
+
"tokens" => nil,
|
|
587
|
+
"model_segments" => [],
|
|
588
|
+
"memory_index" => nil,
|
|
589
|
+
"side" => nil
|
|
590
|
+
}
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# +keys+ of +hash+ whose values are plain (a string or a number), nil
|
|
594
|
+
# for a hash with none.
|
|
595
|
+
def pick(hash, keys)
|
|
596
|
+
return nil unless hash.is_a?(Hash)
|
|
597
|
+
|
|
598
|
+
out = keys.each_with_object({}) do |key, kept|
|
|
599
|
+
value = field(hash, key.to_sym)
|
|
600
|
+
kept[key] = value if value.is_a?(String) || (value.is_a?(Numeric) && value.to_f.finite?)
|
|
601
|
+
end
|
|
602
|
+
out.empty? ? nil : out
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def string_or_nil(value) = value.is_a?(String) ? value : nil
|
|
606
|
+
|
|
607
|
+
def number_or_nil(value) = value.is_a?(Numeric) && value.to_f.finite? ? value : nil
|
|
608
|
+
|
|
310
609
|
# A tool row: the tool's name, and for a file tool the path relative
|
|
311
610
|
# to the session's folder when it is inside it. Never its arguments.
|
|
611
|
+
# The row's +ref+ is the owner's shape (FileRef#to_h: kind, path, the
|
|
612
|
+
# lines) with the relative path, for the main page's refs.js.
|
|
312
613
|
def tool_row(tool, ref, viewer)
|
|
313
614
|
row = { "tool" => tool.to_s }
|
|
314
615
|
path = ref.is_a?(Hash) ? field(ref, :path) : nil
|
|
315
616
|
relative = project_path(path, viewer.cwd, home: viewer.home)
|
|
316
|
-
row
|
|
317
|
-
|
|
617
|
+
return row unless relative
|
|
618
|
+
|
|
619
|
+
file = { "kind" => FileRef::KIND, "path" => relative }
|
|
620
|
+
%i[line end_line].each do |key|
|
|
621
|
+
value = field(ref, key)
|
|
622
|
+
file[key.to_s] = value if value.is_a?(Integer) && value.positive?
|
|
623
|
+
end
|
|
624
|
+
row.merge("ref" => file)
|
|
318
625
|
end
|
|
319
626
|
|
|
320
627
|
# +path+ relative to +cwd+ when it is inside it, else nil. None when
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/* The selection bubble (annotate_bar.js): Annotate + the presets (web.annotate_presets), Copy link last where the page
|
|
2
|
-
has one, small pills with a gap (they wrap on a narrow page).
|
|
3
|
-
participant page (participant.js). Colours are tokens.css's. */
|
|
2
|
+
has one, small pills with a gap (they wrap on a narrow page). index.html links it (app.js makes the bar). Colours are tokens.css's. */
|
|
4
3
|
#annotateBar{position:fixed;z-index:20;display:flex;flex-wrap:wrap;gap:4px;max-width:calc(100vw - 16px)}
|
|
5
4
|
#annotateBar[hidden]{display:none}
|
|
6
5
|
#annotateBar button{font-size:12px;padding:4px 10px;border-radius:999px;box-shadow:0 2px 8px var(--shadow-strong)}
|
|
@@ -47,8 +47,7 @@ import { actionEnabled, actionTitle, sameActions, selectionFields, splitInline }
|
|
|
47
47
|
// bar docks at the bottom of the window
|
|
48
48
|
// @param source (node) => where a selection starting at +node+ came from,
|
|
49
49
|
// {kind, root, …}, or null where the page isn't annotatable
|
|
50
|
-
// (annotations.js annotationSource by default
|
|
51
|
-
// passes its own, participant_annotate.js)
|
|
50
|
+
// (annotations.js annotationSource by default)
|
|
52
51
|
// @param quote (text, source) => the quote block for the selected +text+
|
|
53
52
|
// (annotations.js quoteBlock by default), or null for no text
|
|
54
53
|
// @param link (source, range) => the selection's link, {url, title}, or
|