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
|
@@ -17,9 +17,26 @@ require_relative "../session_metrics"
|
|
|
17
17
|
require_relative "../session_setting"
|
|
18
18
|
require_relative "../system_one/log"
|
|
19
19
|
require_relative "../toolkit"
|
|
20
|
+
require_relative "viewer"
|
|
20
21
|
|
|
21
22
|
module Samagotchi
|
|
22
23
|
module Web
|
|
24
|
+
# What a session's show is read from, the same for the owner's and a
|
|
25
|
+
# participant's (SessionShow#show_reads): the session file, a live
|
|
26
|
+
# worker's turn snapshot and state snapshot (both nil without one), the
|
|
27
|
+
# messages shown (the snapshot's, else the file's) and the stream cursor
|
|
28
|
+
# the page goes on from (+event_seq+, and the snapshot's +event_id+
|
|
29
|
+
# `<seq>-<epoch>`). Each assembler makes its own payload from it.
|
|
30
|
+
ShowReads = Data.define(:id, :session, :turn_snapshot, :state, :event_seq, :event_id, :raw_messages) do
|
|
31
|
+
# The turn in progress, as the worker's snapshot has it (nil: none).
|
|
32
|
+
def current_turn = turn_snapshot && turn_snapshot["current_turn"]
|
|
33
|
+
|
|
34
|
+
def current_turn_id
|
|
35
|
+
turn = current_turn
|
|
36
|
+
turn.is_a?(Hash) ? turn["turn_id"] : nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
23
40
|
# GET /api/sessions/:id, the page's session read, and its light re-reads
|
|
24
41
|
# (?cards=1, ?tail=1, ?timing=1; ?brief=1 is SessionAccessRoutes'
|
|
25
42
|
# handle_brief_read), GET /api/sessions/:id/output, and the timing
|
|
@@ -33,7 +50,7 @@ module Samagotchi
|
|
|
33
50
|
private
|
|
34
51
|
|
|
35
52
|
def handle_show(req, id)
|
|
36
|
-
return
|
|
53
|
+
return participant_show_read(req, id) unless principal(req).owner?
|
|
37
54
|
|
|
38
55
|
# The page's light re-reads: never the whole conversation.
|
|
39
56
|
return handle_cards_read(id) if req.params["cards"] == "1"
|
|
@@ -46,35 +63,59 @@ module Samagotchi
|
|
|
46
63
|
# (The light re-reads come after a full read only.)
|
|
47
64
|
return scratch_refused(id) if session.scratch
|
|
48
65
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
owner_show(req, show_reads(id, session))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The reads a session's show is made of, whoever it is for (ShowReads).
|
|
70
|
+
# Read-only preview: selecting a session never spawns a worker.
|
|
71
|
+
# The worker is woken only on POST /turn (handle_turn) via
|
|
72
|
+
# SessionManager.resume_session + write_turn_input. This avoids
|
|
73
|
+
# replaying last_prompt and spamming workers on preview scrub.
|
|
74
|
+
# event_seq is nil when no live bridge, so the frontend stays
|
|
75
|
+
# silent until the first Send.
|
|
76
|
+
#
|
|
77
|
+
# A live worker's snapshot is the truth (the session file lags its
|
|
78
|
+
# Engine): the messages, the turn in progress and the prompts queued
|
|
79
|
+
# behind it, all at one event_seq the client streams on from.
|
|
80
|
+
# @return [ShowReads]
|
|
81
|
+
def show_reads(id, session)
|
|
59
82
|
live = bridge_get_json(id, "snapshot")
|
|
60
83
|
turn_snapshot = live && live["snapshot"]
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
state = live && live["session_state_snapshot"]
|
|
85
|
+
ShowReads.new(
|
|
86
|
+
id: id, session: session, turn_snapshot: turn_snapshot, state: state,
|
|
87
|
+
event_seq: live_or_saved(state, "event_seq") { bridge_event_seq(id) },
|
|
88
|
+
# The stream cursor `<seq>-<epoch>`: a later worker resets it
|
|
89
|
+
# instead of taking the seq as its own. Only the snapshot has it.
|
|
90
|
+
event_id: turn_snapshot && turn_snapshot["event_id"],
|
|
91
|
+
raw_messages: live_or_saved(turn_snapshot, "messages") { session.messages }
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Every finished turn of +reads+ drawn from a line (a real one, else
|
|
96
|
+
# one made from its messages, #drawn_lines): its items are stamped,
|
|
97
|
+
# and its drawing goes in turn_drawings. The running turn has none yet.
|
|
98
|
+
def show_drawn_lines(reads, timing:, entries:)
|
|
99
|
+
drawn_lines(reads.id, reads.raw_messages, timing: timing, entries: entries, cwd: reads.session.working_directory,
|
|
100
|
+
skip_turn_id: reads.current_turn_id)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The owner's show of +reads+: the whole page's read (its cards,
|
|
104
|
+
# history, commands, recap, timing and question too).
|
|
105
|
+
def owner_show(req, reads)
|
|
106
|
+
id = reads.id
|
|
107
|
+
session = reads.session
|
|
108
|
+
turn_snapshot = reads.turn_snapshot
|
|
109
|
+
raw_messages = reads.raw_messages
|
|
64
110
|
owner = session_owner(id)
|
|
65
111
|
pending = shown_pending_question(session, turn_snapshot, owner)
|
|
66
112
|
# What the worker's server said it served for that model (after a turn);
|
|
67
113
|
# without a worker, what the session's analytics.json last saved.
|
|
68
|
-
persisted =
|
|
69
|
-
session_json = shown_session_json(session,
|
|
70
|
-
|
|
71
|
-
timing = timing_payload(id, live_metrics: snapshot && snapshot["metrics"], persisted: persisted)
|
|
72
|
-
# Every finished turn is drawn from a line (a real one, else one
|
|
73
|
-
# made from its messages): its items are stamped, and its drawing
|
|
74
|
-
# goes in turn_drawings. The running turn has none yet.
|
|
114
|
+
persisted = reads.state ? nil : read_analytics(id)
|
|
115
|
+
session_json = shown_session_json(session, reads.state, owner, persisted)
|
|
116
|
+
timing = timing_payload(id, live_metrics: reads.state && reads.state["metrics"], persisted: persisted)
|
|
75
117
|
raw_cards = live_or_saved(turn_snapshot, "cards") { saved_cards(id, messages: raw_messages) }
|
|
76
|
-
drawn =
|
|
77
|
-
skip_turn_id: current_turn.is_a?(Hash) ? current_turn["turn_id"] : nil)
|
|
118
|
+
drawn = show_drawn_lines(reads, timing: timing, entries: raw_cards)
|
|
78
119
|
messages = messages_for_display(raw_messages, cwd: session.working_directory, drawings: drawn)
|
|
79
120
|
json_response(200, {
|
|
80
121
|
session: session_json,
|
|
@@ -82,7 +123,7 @@ module Samagotchi
|
|
|
82
123
|
messages: messages,
|
|
83
124
|
**turn_drawings_field(drawn, messages),
|
|
84
125
|
failed_turn: failed_turn_for(session, raw_messages),
|
|
85
|
-
current_turn: current_turn,
|
|
126
|
+
current_turn: reads.current_turn,
|
|
86
127
|
# The page's join: the turn in progress and the queue behind it as
|
|
87
128
|
# the live events that draw them (TurnAccumulator.join_events, one
|
|
88
129
|
# unfold, here: any worker's snapshot works).
|
|
@@ -104,11 +145,11 @@ module Samagotchi
|
|
|
104
145
|
markdown_warning: @markdown_renderer.warning,
|
|
105
146
|
markdown_tip: @markdown_renderer.tip,
|
|
106
147
|
**viewer_capabilities(req),
|
|
148
|
+
# Who looks and what they may do (Viewer), as <body> says it at boot.
|
|
149
|
+
viewer: Viewer.for(principal(req)).to_h,
|
|
107
150
|
pending_question: pending,
|
|
108
|
-
last_event_seq:
|
|
109
|
-
|
|
110
|
-
# instead of taking the seq as its own. Only the snapshot has it.
|
|
111
|
-
last_event_id: turn_snapshot && turn_snapshot["event_id"],
|
|
151
|
+
last_event_seq: reads.event_seq,
|
|
152
|
+
last_event_id: reads.event_id,
|
|
112
153
|
timing: timing,
|
|
113
154
|
**objectives_field(id, raw_messages, turn_snapshot)
|
|
114
155
|
})
|
|
@@ -231,6 +272,12 @@ module Samagotchi
|
|
|
231
272
|
served_expected_by: snapshot["served_expected_by"])
|
|
232
273
|
# The model notes its prompt carries now (a /model there changes them).
|
|
233
274
|
session_json = session_json.merge(prompt_notes: snapshot["prompt_notes"]) if snapshot["prompt_notes"].is_a?(Array)
|
|
275
|
+
# The newest context estimate the worker has (a turn in flight: the
|
|
276
|
+
# last request's), so a page opened mid-turn shows the percent at
|
|
277
|
+
# once. Owner show only: a participant's show is ParticipantView's
|
|
278
|
+
# allowlist. A snapshot without the key (an older worker) sends
|
|
279
|
+
# nothing, so its page reads as it always did.
|
|
280
|
+
session_json = session_json.merge(context_status: snapshot["context_status"]) if snapshot.key?("context_status")
|
|
234
281
|
else
|
|
235
282
|
session_json = session_json.merge(saved_served_model(session, persisted))
|
|
236
283
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../client_id"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
module Web
|
|
7
|
+
# Who looks at chi web's main page and what they may do there: the
|
|
8
|
+
# page draws a control, and makes a call, only when +can+ names its
|
|
9
|
+
# capability (viewer.js; docs/internals/web-frontend.md, The viewer).
|
|
10
|
+
# Not Web::Capabilities: that one says what the page renders (markdown
|
|
11
|
+
# display) and whether this request's viewer can open a path in this
|
|
12
|
+
# machine's editor; this one says which of the owner's controls this
|
|
13
|
+
# viewer gets at all. The page reads it from <body> at boot
|
|
14
|
+
# (App#index_data_attributes, #body_data) and from a show's +viewer+.
|
|
15
|
+
#
|
|
16
|
+
# +kind+ "owner" | "participant"; +id+ and +name+ a participant's grant's
|
|
17
|
+
# (nil for the owner); +can+ the frozen list of capability names.
|
|
18
|
+
Viewer = Data.define(:kind, :id, :name, :can) do
|
|
19
|
+
# @param principal [Principal] the request's
|
|
20
|
+
# @return [Viewer] the owner's with every capability; a participant's
|
|
21
|
+
# with PARTICIPANT_CAPABILITIES only (what their routes serve).
|
|
22
|
+
def self.for(principal)
|
|
23
|
+
return Viewer::OWNER if principal.owner?
|
|
24
|
+
|
|
25
|
+
grant = principal.grant
|
|
26
|
+
new(kind: "participant", id: grant.id, name: grant.name, can: Viewer::PARTICIPANT_CAPABILITIES)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def owner? = kind == "owner"
|
|
30
|
+
|
|
31
|
+
# The client id chi web gives this viewer's turns (it replaces the
|
|
32
|
+
# body's): a participant's review:<grant id> (ClientId.review), so the
|
|
33
|
+
# page can tell its own turns; nil for the owner (each tab names its own).
|
|
34
|
+
def client_id = owner? ? nil : ClientId.review(id)
|
|
35
|
+
|
|
36
|
+
def can?(name) = can.include?(name.to_s)
|
|
37
|
+
|
|
38
|
+
# The <body> data attributes the page reads at boot: the kind and the
|
|
39
|
+
# space-separated +can+ list (no names or ids: those come with a show).
|
|
40
|
+
def body_attributes = { "viewer-kind" => kind, "viewer-can" => can.join(" ") }
|
|
41
|
+
|
|
42
|
+
# A participant's carries their client id too.
|
|
43
|
+
def to_h = { kind: kind, id: id, name: name, can: can, **(owner? ? {} : { client_id: client_id }) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The capabilities the main page gates by, each named for what the
|
|
47
|
+
# owner does with it (the page's controls and calls behind each one:
|
|
48
|
+
# docs/internals/web-frontend.md, The viewer).
|
|
49
|
+
Viewer::CAPABILITIES = %w[
|
|
50
|
+
sessions models prompt_history push context working_set working_set_items participants_admin rename images
|
|
51
|
+
session_controls revise apply answer_cards render block_renderers stop_any_turn steer_any_turn
|
|
52
|
+
].freeze
|
|
53
|
+
|
|
54
|
+
# A participant's capabilities: names of their own (the owner's are
|
|
55
|
+
# CAPABILITIES), each one a participant route serves. +send+: their
|
|
56
|
+
# composer (POST …/turn); +stop_own_turn+: the stage's Stop turn on a
|
|
57
|
+
# turn they started (POST …/cancel with its turn_id); +steer_own_turn+:
|
|
58
|
+
# a message sent while their own turn runs goes in at its next step
|
|
59
|
+
# (a worker with Delivery::OWN_TURN_STEER_FEATURE); +revise_own_turn+:
|
|
60
|
+
# Revise on the answer to their own message with only their own turns
|
|
61
|
+
# after it (POST …/turn with revises, ParticipantRevise). And of the owner's,
|
|
62
|
+
# those a participant's route serves filtered: +working_set+ (GET
|
|
63
|
+
# …/working_set, the project's files only) and +images+ (POST …/images
|
|
64
|
+
# within their cap and quota; GET …/images/<name> only for an image
|
|
65
|
+
# they uploaded; their turn takes only those: ParticipantImages).
|
|
66
|
+
Viewer::PARTICIPANT_CAPABILITIES = %w[send stop_own_turn steer_own_turn revise_own_turn working_set images].freeze
|
|
67
|
+
|
|
68
|
+
Viewer::OWNER = Viewer.new(kind: "owner", id: nil, name: nil, can: Viewer::CAPABILITIES)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../working_set"
|
|
4
|
+
require_relative "../llm_context_view"
|
|
5
|
+
require_relative "../session_setting"
|
|
6
|
+
require_relative "../memory_paths"
|
|
7
|
+
require_relative "participant_view"
|
|
8
|
+
|
|
9
|
+
module Samagotchi
|
|
10
|
+
module Web
|
|
11
|
+
# The working set's routes (web.working_set), mixed into App. See
|
|
12
|
+
# docs/internals/working-set.md (Delivery).
|
|
13
|
+
module WorkingSetRoutes
|
|
14
|
+
# What a working-set build reads off a session: its +messages+ (symbol
|
|
15
|
+
# keys at the top, as paired_runs reads them), the +strategy+ its
|
|
16
|
+
# llm_context summary names, its folder (+cwd+) and its +pins+.
|
|
17
|
+
Source = Data.define(:messages, :strategy, :cwd, :pins)
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# GET /api/sessions/:id/working_set: the working set as the model has
|
|
22
|
+
# it ({items:, at:, running:}, WorkingSet), what the page overlays on its rows'
|
|
23
|
+
# set when it seeds a session's and when the expanded view opens (a
|
|
24
|
+
# file changed while the session sat idle). A live worker's snapshot
|
|
25
|
+
# messages are the truth, as #handle_show's, else the file's; the
|
|
26
|
+
# strategy is the session's llm_context summary's, the folder its working
|
|
27
|
+
# directory. running: a turn runs. The snapshot's messages lag it, so
|
|
28
|
+
# the answer is then the turn's last :working_set_updated (the
|
|
29
|
+
# snapshot's working_set, TurnAccumulator#working_set); before the
|
|
30
|
+
# turn emitted one, or from an older worker without the field, it is
|
|
31
|
+
# the session's messages, behind that turn's events, and the page
|
|
32
|
+
# keeps theirs (working_set.js newerSnapshot).
|
|
33
|
+
# A participant's is filtered (ParticipantView.working_set: the
|
|
34
|
+
# project's files only).
|
|
35
|
+
def handle_working_set(req, id)
|
|
36
|
+
session = @session_class.load(id, state_dir: default_state_dir)
|
|
37
|
+
set = working_set_payload(id, session)
|
|
38
|
+
return json_response(200, set) if principal(req).owner?
|
|
39
|
+
|
|
40
|
+
viewer = participant_viewer(req, session)
|
|
41
|
+
# Redacted as the stream's event is (ParticipantView.event): a file name may hold a secret's shape.
|
|
42
|
+
shown = Redact.deep(ParticipantView.working_set(set, viewer), viewer)
|
|
43
|
+
json_response(200, shown.merge("running" => set[:running] == true))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# GET /api/sessions/:id/working_set/item?kind=&key=[&now=1]: what the
|
|
47
|
+
# model has of one item, its timeline (WorkingSet.timeline_of: every
|
|
48
|
+
# run of it, in conversation order), the reader's and the popover
|
|
49
|
+
# peek's. Owner only. Always built from messages, unlike the set's
|
|
50
|
+
# route: the worker's snapshot messages (they lag a running turn's
|
|
51
|
+
# events; the page asks again on its next :working_set_updated), else
|
|
52
|
+
# the file's. Whole texts: the tool limits bound them (read's
|
|
53
|
+
# truncate_at_bytes, the ToolRunner cap). +now+ (a memory): what its
|
|
54
|
+
# latest read would answer now and the diff (WorkingSet.memory_now).
|
|
55
|
+
def handle_working_set_item(req, id)
|
|
56
|
+
kind = req.params["kind"].to_s
|
|
57
|
+
key = req.params["key"].to_s
|
|
58
|
+
unless [WorkingSet::FILE, WorkingSet::MEMORY, WorkingSet::URL].include?(kind) && !key.strip.empty?
|
|
59
|
+
return error_response(400, "invalid_item", "expected kind (file, memory or url) and key")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
session = @session_class.load(id, state_dir: default_state_dir)
|
|
63
|
+
source = working_set_source(bridge_get_json(id, "snapshot"), session)
|
|
64
|
+
timeline = WorkingSet.timeline_of(source.messages, kind: kind, key: key, strategy: source.strategy, cwd: source.cwd)
|
|
65
|
+
body = timeline.to_h
|
|
66
|
+
if req.params["now"] == "1" && kind == WorkingSet::MEMORY
|
|
67
|
+
body[:now] = WorkingSet.memory_now(source.messages, key: key, strategy: source.strategy, cwd: source.cwd,
|
|
68
|
+
root: MemoryPaths.system_dir)&.to_h
|
|
69
|
+
end
|
|
70
|
+
json_response(200, body)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def working_set_payload(id, session)
|
|
74
|
+
live = bridge_get_json(id, "snapshot")
|
|
75
|
+
worker_snapshot = live && live["snapshot"]
|
|
76
|
+
running = !worker_snapshot&.dig("current_turn").nil?
|
|
77
|
+
turn_set = running && running_working_set(live["snapshot"]["working_set"])
|
|
78
|
+
return turn_set.merge(running: true) if turn_set
|
|
79
|
+
|
|
80
|
+
idle_set = idle_working_set(worker_snapshot && worker_snapshot["idle_working_set"]) unless running
|
|
81
|
+
return idle_set.merge(running: false) if idle_set
|
|
82
|
+
|
|
83
|
+
source = working_set_source(live, session)
|
|
84
|
+
snapshot = WorkingSet.build(source.messages, cwd: source.cwd, strategy: source.strategy, pins: source.pins)
|
|
85
|
+
snapshot.to_h.merge(running: running)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# What a build from messages reads (+live+: the worker's Bridge
|
|
89
|
+
# snapshot answer, nil without a worker): the worker's snapshot
|
|
90
|
+
# messages, as #handle_show reads them, else the file's; the strategy
|
|
91
|
+
# the worker's llm_context summary names, else the saved chip's.
|
|
92
|
+
# @return [Source]
|
|
93
|
+
def working_set_source(live, session)
|
|
94
|
+
messages = live_or_saved(live && live["snapshot"], "messages") { session.messages }
|
|
95
|
+
# A snapshot's entries have string keys (paired_runs reads symbols).
|
|
96
|
+
messages = Array(messages).map { |message| message.is_a?(Hash) ? message.transform_keys(&:to_sym) : message }
|
|
97
|
+
llm_context = SessionSetting::LLM_CONTEXT.snapshot_value(live && live["session_state_snapshot"]) do
|
|
98
|
+
SessionSetting::LLM_CONTEXT.saved_summary(session, registry: host_registry)
|
|
99
|
+
end
|
|
100
|
+
Source.new(messages: messages, strategy: working_set_strategy(llm_context), cwd: session.working_directory,
|
|
101
|
+
pins: session.working_set_pins)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The running turn's set from a worker's snapshot ({"items", "at"}), as
|
|
105
|
+
# {items:, at:}; nil when it has none (none emitted yet, an older worker).
|
|
106
|
+
def running_working_set(set)
|
|
107
|
+
return nil unless set.is_a?(Hash) && set["items"].is_a?(Array) && set["at"].is_a?(Integer)
|
|
108
|
+
|
|
109
|
+
{ items: set["items"], at: set["at"] }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The worker's between-turns working set ({"items", "at", optional
|
|
113
|
+
# "adds"}); nil for an older or malformed snapshot.
|
|
114
|
+
def idle_working_set(set)
|
|
115
|
+
return nil unless set.is_a?(Hash) && set["items"].is_a?(Array) && set["at"].is_a?(Integer)
|
|
116
|
+
return nil if set.key?("adds") && !set["adds"].is_a?(Array)
|
|
117
|
+
|
|
118
|
+
{ items: set["items"], at: set["at"] }.tap do |snapshot|
|
|
119
|
+
snapshot[:adds] = set["adds"] if set.key?("adds")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The strategy an llm_context summary names ("none", "stale,forget"), as
|
|
124
|
+
# LLMContextView takes it; none when there is no chip.
|
|
125
|
+
def working_set_strategy(llm_context)
|
|
126
|
+
named = llm_context.is_a?(Hash) ? (llm_context[:strategy] || llm_context["strategy"]).to_s : ""
|
|
127
|
+
layers = named.split(",").map(&:strip).reject { |layer| layer.empty? || layer == "none" }
|
|
128
|
+
layers.empty? ? LLMContextView::NONE : layers.map(&:to_sym)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -1412,8 +1412,9 @@ module Samagotchi
|
|
|
1412
1412
|
@turn_enqueued_id = origin.enqueued_id
|
|
1413
1413
|
verdicts_before = @session.verdicts.size
|
|
1414
1414
|
@reporting_turn = reports_to_parent?(origin)
|
|
1415
|
-
#
|
|
1416
|
-
|
|
1415
|
+
# Who wrote the running turn (a participant's author, nil for the
|
|
1416
|
+
# owner's): its drain merges only their own lines (#pending_input_drain).
|
|
1417
|
+
@turn_author = origin.participant_author
|
|
1417
1418
|
restores = @turn_flow.restores
|
|
1418
1419
|
begin
|
|
1419
1420
|
@checkpoint.begin_turn(prompt: prompt, turn_id: turn_args[:id], enqueued_id: origin.enqueued_id, author: turn_author_fields(origin))
|
|
@@ -1603,15 +1604,12 @@ module Samagotchi
|
|
|
1603
1604
|
# one (steering merges text only): skipped here, not a barrier, so a
|
|
1604
1605
|
# line sent after it still goes in at this boundary. Only the files
|
|
1605
1606
|
# claimed below reach @merged_this_turn and the checkpoint.
|
|
1606
|
-
# A participant's
|
|
1607
|
-
# Bridge queues them, this covers a file it didn't
|
|
1608
|
-
#
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
SessionInbox.find_new_input_files(@session_dir).sort
|
|
1613
|
-
.reject { |input_file| SessionInbox.waits_for_turn_end?(input_file) }
|
|
1614
|
-
end
|
|
1607
|
+
# A participant's turn takes only their own lines: the owner's and
|
|
1608
|
+
# anyone else's (the Bridge queues them, this covers a file it didn't
|
|
1609
|
+
# see) run as their own turns after it, never inside its boundary;
|
|
1610
|
+
# and the owner's turn takes no participant's (SessionInbox.turn_end_data?).
|
|
1611
|
+
files = SessionInbox.find_new_input_files(@session_dir).sort
|
|
1612
|
+
.reject { |input_file| SessionInbox.waits_for_turn_end?(input_file, running_author: @turn_author) }
|
|
1615
1613
|
merged = files.filter_map do |input_file|
|
|
1616
1614
|
claimed_file = SessionInbox.claim_input_file(input_file)
|
|
1617
1615
|
next unless claimed_file
|
|
@@ -1624,6 +1622,13 @@ module Samagotchi
|
|
|
1624
1622
|
next
|
|
1625
1623
|
end
|
|
1626
1624
|
begin
|
|
1625
|
+
# A participant's line gets the checks their queued turn gets at
|
|
1626
|
+
# its start (#participant_refusal: the link, the cap): refused,
|
|
1627
|
+
# it is taken off as that turn would be, and never merged.
|
|
1628
|
+
if (author = input.origin.participant_author) && (reason = participant_refusal(author))
|
|
1629
|
+
refuse_participant_turn(input.origin, reason)
|
|
1630
|
+
next
|
|
1631
|
+
end
|
|
1627
1632
|
prompt = input.prompt.to_s.strip
|
|
1628
1633
|
# Which turn took it, on disk before its file goes (a wait for
|
|
1629
1634
|
# it reads this, not an inference: ReplyWait::Poll#follow_merge).
|
|
@@ -1660,10 +1665,10 @@ module Samagotchi
|
|
|
1660
1665
|
end
|
|
1661
1666
|
|
|
1662
1667
|
# Whether a person's message waits that the next boundary merges
|
|
1663
|
-
# (SessionInbox.steer_waiting?): a running task_wait ends on it.
|
|
1664
|
-
#
|
|
1668
|
+
# (SessionInbox.steer_waiting?): a running task_wait ends on it. In a
|
|
1669
|
+
# participant's turn only their own.
|
|
1665
1670
|
def input_waiting?
|
|
1666
|
-
|
|
1671
|
+
SessionInbox.steer_waiting?(@session_dir, running_author: @turn_author)
|
|
1667
1672
|
end
|
|
1668
1673
|
|
|
1669
1674
|
# A participant's author fields (Engine#author_fields), {} for the owner.
|