samagotchi 0.16.0 → 0.18.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 +149 -1
- data/README.md +2 -0
- data/docs/architecture.md +38 -2
- data/docs/cli.md +37 -9
- data/docs/configuration.md +8 -6
- data/docs/guardrails.md +51 -8
- data/docs/hooks.md +4 -1
- data/docs/internals/background-tasks.md +1 -0
- data/docs/memory.md +41 -0
- data/docs/plugins.md +5 -2
- data/docs/sessions.md +8 -6
- data/docs/sub-agent.md +43 -4
- data/docs/testing.md +47 -0
- data/lib/samagotchi/answer_command.rb +23 -2
- data/lib/samagotchi/answer_tail.rb +52 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +20 -5
- data/lib/samagotchi/bridge.rb +86 -14
- data/lib/samagotchi/bridge_client.rb +18 -3
- data/lib/samagotchi/bundle_command.rb +121 -4
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +18 -3
- data/lib/samagotchi/bundles/guardrails/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -0
- data/lib/samagotchi/commands/registry.rb +28 -0
- data/lib/samagotchi/config.rb +7 -1
- data/lib/samagotchi/continue_offer.rb +177 -0
- data/lib/samagotchi/engine.rb +73 -19
- data/lib/samagotchi/guardrails/outside.rb +46 -0
- data/lib/samagotchi/guardrails/parent_approvals.rb +2 -0
- data/lib/samagotchi/guardrails/parent_continue.rb +43 -0
- data/lib/samagotchi/guardrails/rules.rb +17 -5
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +267 -0
- data/lib/samagotchi/guardrails/targets.rb +30 -13
- data/lib/samagotchi/guardrails.rb +2 -0
- data/lib/samagotchi/iteration_limit.rb +38 -0
- data/lib/samagotchi/kernel_loop.rb +2 -1
- data/lib/samagotchi/llm/backend.rb +2 -1
- data/lib/samagotchi/llm/chat_loop.rb +2 -1
- data/lib/samagotchi/llm/native_backend.rb +2 -1
- data/lib/samagotchi/memory_bundle/builder.rb +22 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +17 -6
- data/lib/samagotchi/memory_bundle/installer.rb +60 -21
- data/lib/samagotchi/memory_bundle/profile.rb +9 -4
- data/lib/samagotchi/memory_bundle/provenance.rb +14 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +5 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +5 -3
- data/lib/samagotchi/memory_bundle/trash.rb +136 -0
- data/lib/samagotchi/memory_bundle/uninstaller.rb +34 -5
- data/lib/samagotchi/parent_report.rb +39 -9
- data/lib/samagotchi/plugin/context.rb +14 -2
- data/lib/samagotchi/question_desk.rb +205 -46
- data/lib/samagotchi/reply_wait.rb +10 -4
- data/lib/samagotchi/self_report.rb +49 -3
- data/lib/samagotchi/send_command.rb +46 -6
- data/lib/samagotchi/session.rb +3 -2
- data/lib/samagotchi/session_commands.rb +3 -2
- data/lib/samagotchi/session_manager.rb +4 -1
- data/lib/samagotchi/session_metrics.rb +137 -48
- data/lib/samagotchi/sessions_command.rb +3 -2
- data/lib/samagotchi/system_prompt.rb +46 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +39 -6
- data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -1
- data/lib/samagotchi/terminal_ui/question_prompt.rb +34 -2
- data/lib/samagotchi/terminal_ui.rb +35 -2
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tools/builtins.rb +11 -3
- data/lib/samagotchi/tools/delegate_relay.rb +4 -0
- data/lib/samagotchi/tools/delegate_wait.rb +14 -5
- data/lib/samagotchi/tools/edit.rb +6 -1
- data/lib/samagotchi/tools/execute.rb +5 -4
- data/lib/samagotchi/tools/peers.rb +3 -2
- data/lib/samagotchi/tools/read.rb +2 -1
- data/lib/samagotchi/tools/task_runtime.rb +1 -1
- data/lib/samagotchi/turn_flow.rb +8 -7
- data/lib/samagotchi/turn_note.rb +7 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +189 -43
- data/lib/samagotchi/web/public/app.js +157 -51
- data/lib/samagotchi/web/public/data.js +12 -13
- data/lib/samagotchi/web/public/index.html +6 -1
- data/lib/samagotchi/web/public/notify.js +12 -5
- data/lib/samagotchi/web/public/question_card.js +28 -1
- data/lib/samagotchi/web/public/question_cards.js +74 -3
- data/lib/samagotchi/web/public/sessions_list.js +1 -0
- data/lib/samagotchi/web/public/stage_view.js +13 -1
- data/lib/samagotchi/web/public/strip.js +11 -0
- data/lib/samagotchi/web/public/timing.js +67 -12
- data/lib/samagotchi/web/public/turn_events.js +74 -3
- data/lib/samagotchi/web/public/update_notice.js +15 -0
- data/lib/samagotchi/worker.rb +58 -60
- data/lib/samagotchi/worker_sidecar.rb +7 -1
- metadata +10 -1
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -9,11 +9,13 @@ require "uri"
|
|
|
9
9
|
require "rack"
|
|
10
10
|
require "rack/request"
|
|
11
11
|
|
|
12
|
+
require_relative "../answer_tail"
|
|
12
13
|
require_relative "../bridge_client"
|
|
13
14
|
require_relative "../bridge/bounded_queue"
|
|
14
15
|
require_relative "../bridge/card_store"
|
|
15
16
|
require_relative "../session"
|
|
16
17
|
require_relative "../session_manager"
|
|
18
|
+
require_relative "../session_metrics"
|
|
17
19
|
require_relative "../session_commands"
|
|
18
20
|
require_relative "../steer"
|
|
19
21
|
require_relative "../host_registry"
|
|
@@ -177,14 +179,18 @@ module Samagotchi
|
|
|
177
179
|
|
|
178
180
|
# API requests at debug level (not the page and its assets): method,
|
|
179
181
|
# path, status, time. Never a body or the query; only a tail=1 read
|
|
180
|
-
# (the page's end-of-turn re-read)
|
|
182
|
+
# (the page's end-of-turn re-read) and a timing=1 one (its fallback
|
|
183
|
+
# when the merge came up short) are marked, to tell them from a full one.
|
|
181
184
|
def log_request(env, response, started)
|
|
182
185
|
path = env["PATH_INFO"].to_s
|
|
183
186
|
return unless path.start_with?("/api/") && Log.level?(:debug)
|
|
184
187
|
|
|
185
|
-
|
|
188
|
+
params = env["QUERY_STRING"].to_s.split("&")
|
|
189
|
+
marks = {}
|
|
190
|
+
marks[:tail] = true if params.include?("tail=1")
|
|
191
|
+
marks[:timing] = true if params.include?("timing=1")
|
|
186
192
|
Log.debug(:web, "request", sid: path[%r{\A/api/sessions/([^/]+)}, 1], method: env["REQUEST_METHOD"], path: path,
|
|
187
|
-
status: response[0], **
|
|
193
|
+
status: response[0], **marks,
|
|
188
194
|
ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round)
|
|
189
195
|
end
|
|
190
196
|
|
|
@@ -518,6 +524,11 @@ module Samagotchi
|
|
|
518
524
|
end
|
|
519
525
|
|
|
520
526
|
def handle_show(req, id)
|
|
527
|
+
# The page's light re-reads: never the whole conversation.
|
|
528
|
+
return handle_cards_read(id) if req.params["cards"] == "1"
|
|
529
|
+
return handle_tail_read(req, id) if req.params["tail"] == "1"
|
|
530
|
+
return handle_timing_read(id) if req.params["timing"] == "1"
|
|
531
|
+
|
|
521
532
|
session = @session_class.load(id, state_dir: default_state_dir)
|
|
522
533
|
# Read-only preview: selecting a session never spawns a worker.
|
|
523
534
|
# The worker is woken only on POST /turn (handle_turn) via
|
|
@@ -531,11 +542,6 @@ module Samagotchi
|
|
|
531
542
|
# behind it, all at one event_seq the client streams on from.
|
|
532
543
|
live = bridge_get_json(id, "snapshot")
|
|
533
544
|
turn_snapshot = live && live["snapshot"]
|
|
534
|
-
# ?cards=1: the page's re-read when a card arrives, for its rendered
|
|
535
|
-
# body (the stream carries the card's plain text only).
|
|
536
|
-
if req.params["cards"] == "1"
|
|
537
|
-
return json_response(200, { cards: cards_for_display(turn_snapshot, id) })
|
|
538
|
-
end
|
|
539
545
|
snapshot = live && live["session_state_snapshot"]
|
|
540
546
|
last_event_seq = snapshot ? snapshot["event_seq"] : bridge_event_seq(id)
|
|
541
547
|
current_turn = turn_snapshot && turn_snapshot["current_turn"]
|
|
@@ -543,8 +549,9 @@ module Samagotchi
|
|
|
543
549
|
# The file's question only while a worker owns the session: one a
|
|
544
550
|
# dead worker saved can't be answered (its turn is gone), nor a chi
|
|
545
551
|
# REPL's (it shares nothing with the web).
|
|
552
|
+
# A question between turns (the step-limit one) is the snapshot's own.
|
|
546
553
|
pending = if turn_snapshot
|
|
547
|
-
current_turn
|
|
554
|
+
current_turn ? current_turn["pending_question"] : turn_snapshot["pending_question"]
|
|
548
555
|
elsif owner&.worker?
|
|
549
556
|
session.pending_question
|
|
550
557
|
end
|
|
@@ -557,19 +564,6 @@ module Samagotchi
|
|
|
557
564
|
end
|
|
558
565
|
raw_messages = turn_snapshot ? turn_snapshot["messages"] : session.messages
|
|
559
566
|
timing = timing_payload(id, live_metrics: snapshot && snapshot["metrics"])
|
|
560
|
-
# ?tail=1: the page's re-read at the end of a turn wants the final
|
|
561
|
-
# answer's rendered markdown, the status and the timing, not the
|
|
562
|
-
# whole history again (nor a render of every earlier answer).
|
|
563
|
-
if req.params["tail"] == "1"
|
|
564
|
-
return json_response(200, {
|
|
565
|
-
tail: true,
|
|
566
|
-
session: session_json,
|
|
567
|
-
messages: last_assistant_for_display(raw_messages),
|
|
568
|
-
markdown_warning: @markdown_renderer.warning,
|
|
569
|
-
timing: timing
|
|
570
|
-
})
|
|
571
|
-
end
|
|
572
|
-
|
|
573
567
|
json_response(200, {
|
|
574
568
|
session: session_json,
|
|
575
569
|
history: read_history(id),
|
|
@@ -585,7 +579,7 @@ module Samagotchi
|
|
|
585
579
|
plugin_warning: turn_snapshot && turn_snapshot["plugin_warning"],
|
|
586
580
|
# Plugins' slow setup still running (chi.init): the page's init row.
|
|
587
581
|
init_tasks: turn_snapshot ? Array(turn_snapshot["init_tasks"]) : [],
|
|
588
|
-
cards: cards_for_display(turn_snapshot
|
|
582
|
+
cards: cards_for_display(turn_snapshot ? turn_snapshot["cards"] : saved_cards(id)),
|
|
589
583
|
# The composer's / autocomplete; the built-ins until a worker
|
|
590
584
|
# names its plugins' too.
|
|
591
585
|
commands: turn_snapshot&.fetch("commands", nil) || SessionCommands.builtin_registry.listing,
|
|
@@ -601,14 +595,101 @@ module Samagotchi
|
|
|
601
595
|
error_response(404, "not_found", e.message)
|
|
602
596
|
end
|
|
603
597
|
|
|
598
|
+
# ?cards=1: the page's re-read when a card arrives, for its rendered
|
|
599
|
+
# body (the stream carries the card's plain text only). The worker's
|
|
600
|
+
# cards from its light GET tail, else the saved ones; the session file
|
|
601
|
+
# is only stat'ed (the 404), never parsed.
|
|
602
|
+
def handle_cards_read(id)
|
|
603
|
+
return error_response(404, "not_found", "Session not found: #{id}") unless session_exists?(id)
|
|
604
|
+
|
|
605
|
+
tail = live_tail(id)
|
|
606
|
+
json_response(200, { cards: cards_for_display(tail ? tail["cards"] : saved_cards(id)) })
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# ?tail=1: the page's re-read at the end of a turn wants the final
|
|
610
|
+
# answer's rendered markdown, the status and the timing, not the whole
|
|
611
|
+
# history again (nor a render of every earlier answer). Live: the
|
|
612
|
+
# worker's GET tail alone (no session file read); else the file. The
|
|
613
|
+
# session part is what the page reads of it (id, status, used memory
|
|
614
|
+
# names). ?turn_id=: that turn's answer (a queued turn's page re-reads
|
|
615
|
+
# after the next one started). ?recent=1 (the page since this
|
|
616
|
+
# release): the timing's records are the newest turn's (#recent_timing),
|
|
617
|
+
# which the page merges by id; without it, the whole timing as before.
|
|
618
|
+
def handle_tail_read(req, id)
|
|
619
|
+
return error_response(404, "not_found", "Session not found: #{id}") unless session_exists?(id)
|
|
620
|
+
|
|
621
|
+
turn_id = req.params["turn_id"].to_s.strip
|
|
622
|
+
turn_id = nil if turn_id.empty?
|
|
623
|
+
tail = live_tail(id, turn_id: turn_id)
|
|
624
|
+
state = tail && tail["session_state_snapshot"]
|
|
625
|
+
if state.is_a?(Hash)
|
|
626
|
+
session_json = { id: id, status: state["status"], used_memory_names: Array(state["used_memory_names"]) }
|
|
627
|
+
messages = last_assistant_for_display([tail["answer"]].compact)
|
|
628
|
+
else
|
|
629
|
+
session = @session_class.load(id, state_dir: default_state_dir)
|
|
630
|
+
state = nil
|
|
631
|
+
session_json = { id: id, status: displayed_status(session), used_memory_names: Array(session.used_memory_names) }
|
|
632
|
+
messages = last_assistant_for_display(session.messages, turn_id: turn_id)
|
|
633
|
+
end
|
|
634
|
+
live_metrics = state && state["metrics"]
|
|
635
|
+
# Without recent=1: a tab opened before this release (it replaces its
|
|
636
|
+
# timing with the reply's). Remove this branch after the next release.
|
|
637
|
+
timing = req.params["recent"] == "1" ? recent_timing(id, live_metrics) : timing_payload(id, live_metrics: live_metrics)
|
|
638
|
+
json_response(200, {
|
|
639
|
+
tail: true,
|
|
640
|
+
session: session_json,
|
|
641
|
+
messages: messages,
|
|
642
|
+
markdown_warning: @markdown_renderer.warning,
|
|
643
|
+
timing: timing
|
|
644
|
+
})
|
|
645
|
+
rescue ArgumentError => e
|
|
646
|
+
error_response(404, "not_found", e.message)
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# ?timing=1: the whole timing (every record, analytics.json merged with
|
|
650
|
+
# the live ones) and turn_count, when the page's merge comes up short.
|
|
651
|
+
# The worker's GET state, never its messages.
|
|
652
|
+
def handle_timing_read(id)
|
|
653
|
+
return error_response(404, "not_found", "Session not found: #{id}") unless session_exists?(id)
|
|
654
|
+
|
|
655
|
+
state = bridge_get_json(id, "state")
|
|
656
|
+
json_response(200, { timing: timing_payload(id, live_metrics: state && state["session_state_snapshot"]&.fetch("metrics", nil)) })
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def session_exists?(id)
|
|
660
|
+
@session_class.exist?(id, state_dir: default_state_dir)
|
|
661
|
+
rescue ArgumentError
|
|
662
|
+
false
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# The worker's light read (Bridge GET tail): {"session_state_snapshot",
|
|
666
|
+
# "answer", "cards", "event_seq", "event_id"}. A worker from before the
|
|
667
|
+
# route answers 404 not_found: its GET snapshot, in the same shape (the
|
|
668
|
+
# answer by AnswerTail). Anything else (no worker, a refused connect, a
|
|
669
|
+
# 500, a timeout) is nil: the caller reads the disk, with no second
|
|
670
|
+
# try at a worker that failed once.
|
|
671
|
+
# @return [Hash, nil]
|
|
672
|
+
def live_tail(id, turn_id: nil)
|
|
673
|
+
status, body = bridge_get(id, turn_id ? "tail?turn_id=#{URI.encode_www_form_component(turn_id)}" : "tail")
|
|
674
|
+
return body if status == 200 && body.is_a?(Hash)
|
|
675
|
+
return nil unless status == 404 && body.is_a?(Hash) && body["error"] == "not_found"
|
|
676
|
+
|
|
677
|
+
status, live = bridge_get(id, "snapshot")
|
|
678
|
+
return nil unless status == 200 && live.is_a?(Hash) && live["snapshot"].is_a?(Hash)
|
|
679
|
+
|
|
680
|
+
snap = live["snapshot"]
|
|
681
|
+
{ "session_state_snapshot" => live["session_state_snapshot"],
|
|
682
|
+
"answer" => AnswerTail.find(snap["messages"], turn_id: turn_id),
|
|
683
|
+
"cards" => snap["cards"], "event_seq" => snap["event_seq"], "event_id" => snap["event_id"] }
|
|
684
|
+
end
|
|
685
|
+
|
|
604
686
|
# The worker's last cards and between-turns notices (Bridge
|
|
605
|
-
#
|
|
606
|
-
# saved in the session's folder (
|
|
687
|
+
# cards: CardStore#list), else (no live worker) the ones the last
|
|
688
|
+
# worker saved in the session's folder (#saved_cards), a card's body as
|
|
607
689
|
# body_html: rendered markdown when the renderer is on, else the
|
|
608
690
|
# escaped text in a <pre>.
|
|
609
691
|
# @return [Array<Hash>]
|
|
610
|
-
def cards_for_display(
|
|
611
|
-
cards = turn_snapshot ? turn_snapshot["cards"] : saved_cards(id)
|
|
692
|
+
def cards_for_display(cards)
|
|
612
693
|
Array(cards).filter_map do |card|
|
|
613
694
|
next unless card.is_a?(Hash)
|
|
614
695
|
next card unless card["type"].to_s == "card"
|
|
@@ -979,7 +1060,8 @@ module Samagotchi
|
|
|
979
1060
|
end
|
|
980
1061
|
|
|
981
1062
|
# GET /api/events: the session list as one SSE stream per tab. The
|
|
982
|
-
# first frame is the hub's snapshot (scoped by ?dir= as the list is
|
|
1063
|
+
# first frame is the hub's snapshot (scoped by ?dir= as the list is,
|
|
1064
|
+
# with chi's version),
|
|
983
1065
|
# then `session` for an upsert and `session_gone` for a removal, with
|
|
984
1066
|
# a `: ping` while idle. Frame ids are the hub's seq, for the log's
|
|
985
1067
|
# sake: there is no replay, a reconnect starts with a fresh snapshot.
|
|
@@ -1059,12 +1141,22 @@ module Samagotchi
|
|
|
1059
1141
|
port && BridgeClient.new(session_id: session_id, port: port, host: DEFAULT_HOST)
|
|
1060
1142
|
end
|
|
1061
1143
|
|
|
1062
|
-
#
|
|
1144
|
+
# GET a read path of the session's live Bridge (+path+: "state",
|
|
1145
|
+
# "snapshot", "stats", ...).
|
|
1063
1146
|
# @return [Hash, nil] parsed JSON body, or nil when no live bridge / timeout.
|
|
1064
1147
|
def bridge_get_json(session_id, path)
|
|
1065
1148
|
bridge_client(session_id)&.get_json(path)
|
|
1066
1149
|
end
|
|
1067
1150
|
|
|
1151
|
+
# GET a read path of the session's live Bridge with its status
|
|
1152
|
+
# (BridgeClient#get).
|
|
1153
|
+
# @return [Array(Integer, Object)] [status, parsed body]; [nil, nil]
|
|
1154
|
+
# with no live bridge or no reply
|
|
1155
|
+
def bridge_get(session_id, path)
|
|
1156
|
+
client = bridge_client(session_id)
|
|
1157
|
+
client ? client.get(path) : [nil, nil]
|
|
1158
|
+
end
|
|
1159
|
+
|
|
1068
1160
|
# Monotonic SSE cursor for a live bridge session, else nil.
|
|
1069
1161
|
def bridge_event_seq(session_id)
|
|
1070
1162
|
bridge_client(session_id)&.event_seq
|
|
@@ -1137,9 +1229,12 @@ module Samagotchi
|
|
|
1137
1229
|
session_dir: @session_class.session_dir(s.id, state_dir: default_state_dir))
|
|
1138
1230
|
end
|
|
1139
1231
|
|
|
1232
|
+
# The whole timing: analytics.json's records merged with a live
|
|
1233
|
+
# worker's (by id), and turn_count, the finished turns (#turn_count).
|
|
1140
1234
|
def timing_payload(session_id, live_metrics: nil)
|
|
1141
1235
|
persisted = read_analytics(session_id)
|
|
1142
|
-
|
|
1236
|
+
live = live_metrics.is_a?(Hash)
|
|
1237
|
+
source = if live
|
|
1143
1238
|
persisted.merge(live_metrics).merge(
|
|
1144
1239
|
"started_at" => persisted["started_at"] || live_metrics["started_at"],
|
|
1145
1240
|
"turn_records" => merge_timing_records(persisted["turn_records"], live_metrics["turn_records"]),
|
|
@@ -1148,14 +1243,66 @@ module Samagotchi
|
|
|
1148
1243
|
else
|
|
1149
1244
|
persisted
|
|
1150
1245
|
end
|
|
1246
|
+
turns = Array(source["turn_records"])
|
|
1247
|
+
timing_from(source, turns, Array(source["tool_records"]), live: live,
|
|
1248
|
+
turn_count: (live && turn_count(live_metrics)) || turns.size)
|
|
1249
|
+
end
|
|
1250
|
+
|
|
1251
|
+
# The timing a ?tail=1&recent=1 read sends: a live worker's recent
|
|
1252
|
+
# records as its metrics hold them (the newest finished turn's and
|
|
1253
|
+
# the unsaved ones, the running turn's finished calls), no
|
|
1254
|
+
# analytics.json read; with no worker the disk's, trimmed to the
|
|
1255
|
+
# newest turn. turn_count tells the page whether its merge missed one
|
|
1256
|
+
# (it then reads ?timing=1).
|
|
1257
|
+
def recent_timing(session_id, live_metrics)
|
|
1258
|
+
if live_metrics.is_a?(Hash)
|
|
1259
|
+
count = turn_count(live_metrics)
|
|
1260
|
+
# A metrics hash without the count: the whole timing.
|
|
1261
|
+
return timing_payload(session_id, live_metrics: live_metrics) unless count
|
|
1262
|
+
|
|
1263
|
+
turns = Array(live_metrics["turn_records"])
|
|
1264
|
+
tools = Array(live_metrics["tool_records"])
|
|
1265
|
+
# An older worker's metrics hold every record (before they held the
|
|
1266
|
+
# recent ones only, never more than LIVE_RECORDS_CAP turns).
|
|
1267
|
+
turns, tools = newest_turn_records(turns, tools, live_metrics["active_turn"]) if turns.size > SessionMetrics::LIVE_RECORDS_CAP
|
|
1268
|
+
return timing_from(live_metrics, turns, tools, live: true, turn_count: count)
|
|
1269
|
+
end
|
|
1270
|
+
|
|
1271
|
+
persisted = read_analytics(session_id)
|
|
1272
|
+
all = Array(persisted["turn_records"])
|
|
1273
|
+
turns, tools = newest_turn_records(all, Array(persisted["tool_records"]), nil)
|
|
1274
|
+
timing_from(persisted, turns, tools, live: false, turn_count: all.size)
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
# The finished turns a live worker counts: its metrics' turns (the
|
|
1278
|
+
# loaded history, its own and the running one) less the running one.
|
|
1279
|
+
# @return [Integer, nil]
|
|
1280
|
+
def turn_count(metrics)
|
|
1281
|
+
turns = metrics["turns"]
|
|
1282
|
+
return nil unless turns.is_a?(Integer)
|
|
1283
|
+
|
|
1284
|
+
[turns - (metrics["active_turn"] ? 1 : 0), 0].max
|
|
1285
|
+
end
|
|
1286
|
+
|
|
1287
|
+
# The newest turn record and its tool records (and the running turn's
|
|
1288
|
+
# finished calls).
|
|
1289
|
+
def newest_turn_records(turns, tools, active_turn)
|
|
1290
|
+
kept = turns.last(1)
|
|
1291
|
+
ids = kept.filter_map { |r| r["id"] if r.is_a?(Hash) }
|
|
1292
|
+
ids << active_turn["id"] if active_turn.is_a?(Hash) && active_turn["id"]
|
|
1293
|
+
[kept, tools.select { |r| r.is_a?(Hash) && ids.include?(r["turn_id"]) }]
|
|
1294
|
+
end
|
|
1295
|
+
|
|
1296
|
+
def timing_from(source, turn_records, tool_records, live:, turn_count:)
|
|
1151
1297
|
started_at = source["started_at"]
|
|
1152
1298
|
last_activity_at = source["last_activity_at"]
|
|
1153
1299
|
{
|
|
1154
1300
|
started_at: started_at,
|
|
1155
1301
|
last_activity_at: last_activity_at,
|
|
1156
|
-
session_duration_ms: session_duration_ms(started_at, last_activity_at, active:
|
|
1157
|
-
turn_records:
|
|
1158
|
-
tool_records:
|
|
1302
|
+
session_duration_ms: session_duration_ms(started_at, last_activity_at, active: live),
|
|
1303
|
+
turn_records: turn_records,
|
|
1304
|
+
tool_records: tool_records,
|
|
1305
|
+
turn_count: turn_count,
|
|
1159
1306
|
active_turn: source["active_turn"],
|
|
1160
1307
|
active_tools: Array(source["active_tools"]),
|
|
1161
1308
|
# The context last seen and the token sums, for the ctx meter
|
|
@@ -1331,14 +1478,10 @@ module Samagotchi
|
|
|
1331
1478
|
MessageParts.for_message(list[index], responses, cwd: cwd)
|
|
1332
1479
|
end
|
|
1333
1480
|
|
|
1334
|
-
# The last message messages_for_display shows as an answer
|
|
1335
|
-
# at most one:
|
|
1336
|
-
def last_assistant_for_display(msgs)
|
|
1337
|
-
|
|
1338
|
-
shown = messages_for_display([m]).first
|
|
1339
|
-
return [shown] if shown && shown[:role] == "assistant"
|
|
1340
|
-
end
|
|
1341
|
-
[]
|
|
1481
|
+
# The last message messages_for_display shows as an answer
|
|
1482
|
+
# (AnswerTail), as a list of at most one: only that one is rendered.
|
|
1483
|
+
def last_assistant_for_display(msgs, turn_id: nil)
|
|
1484
|
+
messages_for_display([AnswerTail.find(msgs, turn_id: turn_id)].compact)
|
|
1342
1485
|
end
|
|
1343
1486
|
|
|
1344
1487
|
def parse_json(str)
|
|
@@ -1383,7 +1526,9 @@ module Samagotchi
|
|
|
1383
1526
|
# folder in a repo, that project's name and root. A bad dir adds
|
|
1384
1527
|
# nothing here: the list call answers 400 and the page says so.
|
|
1385
1528
|
def index_data_attributes(req)
|
|
1386
|
-
|
|
1529
|
+
# version: the chi that served the page; the events snapshot names
|
|
1530
|
+
# the one serving now (an upgraded chi web after a reconnect).
|
|
1531
|
+
attrs = { "sessions-dir" => sessions_dir_label, "server-dir" => home_label(Dir.pwd), "version" => Samagotchi::VERSION }
|
|
1387
1532
|
attrs["view"] = view_name(req.params["view"])
|
|
1388
1533
|
# Always there: an empty one means no presets, not the default.
|
|
1389
1534
|
attrs["annotate-presets"] = @annotate_presets
|
|
@@ -1456,7 +1601,8 @@ module Samagotchi
|
|
|
1456
1601
|
end
|
|
1457
1602
|
|
|
1458
1603
|
def each
|
|
1459
|
-
|
|
1604
|
+
# version: the page compares it with the one it was served by.
|
|
1605
|
+
yield frame(nil, "snapshot", sessions: @snapshot, version: Samagotchi::VERSION)
|
|
1460
1606
|
last_write = monotonic
|
|
1461
1607
|
loop do
|
|
1462
1608
|
event = @queue.pop([@heartbeat, EVENTS_POLL].min)
|