samagotchi 0.75.0 → 0.77.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 +72 -1
- data/docs/architecture.md +11 -4
- data/docs/cli.md +13 -1
- data/docs/configuration.md +4 -3
- data/docs/guardrails.md +9 -0
- data/docs/internals/anthropic.md +3 -1
- data/docs/internals/bundles.md +20 -7
- data/docs/internals/context-telemetry.md +22 -0
- data/docs/internals/engine.md +15 -4
- data/docs/internals/prompt-caching.md +2 -1
- data/docs/memory.md +34 -0
- data/docs/plugins.md +43 -1
- data/docs/releasing.md +6 -2
- data/docs/sessions.md +3 -1
- data/lib/samagotchi/answer_command.rb +5 -5
- data/lib/samagotchi/bridge.rb +3 -3
- data/lib/samagotchi/bundle_command.rb +52 -39
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +19 -2
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +12 -10
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +1 -1
- data/lib/samagotchi/desktop/macos.rb +1 -2
- data/lib/samagotchi/effective_model.rb +5 -0
- data/lib/samagotchi/engine.rb +105 -16
- data/lib/samagotchi/guardrails/parent_approvals.rb +4 -4
- data/lib/samagotchi/guardrails/parent_continue.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +13 -10
- data/lib/samagotchi/llm_context_override.rb +8 -6
- data/lib/samagotchi/memory_bundle/asset_installer.rb +34 -5
- data/lib/samagotchi/memory_bundle/builder.rb +13 -14
- data/lib/samagotchi/memory_bundle/installer.rb +122 -5
- data/lib/samagotchi/memory_bundle/manifest.rb +20 -1
- data/lib/samagotchi/memory_bundle/memory_files.rb +24 -6
- data/lib/samagotchi/memory_bundle/profile.rb +1 -1
- data/lib/samagotchi/memory_bundle/shipped_update.rb +2 -1
- data/lib/samagotchi/memory_bundle/source.rb +66 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +4 -2
- data/lib/samagotchi/message_parts.rb +272 -0
- data/lib/samagotchi/model_price.rb +4 -3
- data/lib/samagotchi/parent_report.rb +30 -22
- data/lib/samagotchi/paths.rb +15 -1
- data/lib/samagotchi/pending_question.rb +49 -0
- data/lib/samagotchi/question_desk.rb +39 -32
- data/lib/samagotchi/relay.rb +8 -9
- data/lib/samagotchi/relay_verifier.rb +2 -2
- data/lib/samagotchi/relay_watcher.rb +2 -2
- data/lib/samagotchi/reply_wait.rb +4 -4
- data/lib/samagotchi/row_refs.rb +1 -1
- data/lib/samagotchi/session.rb +22 -14
- data/lib/samagotchi/session_commands.rb +5 -0
- data/lib/samagotchi/session_manager.rb +1 -2
- data/lib/samagotchi/session_metrics/model_segment.rb +109 -0
- data/lib/samagotchi/session_metrics/totals.rb +127 -0
- data/lib/samagotchi/session_metrics.rb +118 -83
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +3 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +24 -10
- data/lib/samagotchi/terminal_ui/model_segments_text.rb +118 -0
- data/lib/samagotchi/terminal_ui/status_row.rb +3 -1
- data/lib/samagotchi/terminal_ui.rb +5 -3
- data/lib/samagotchi/tool_output_cut.rb +1 -1
- data/lib/samagotchi/tool_response.rb +13 -13
- data/lib/samagotchi/tool_row_fields.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +16 -9
- data/lib/samagotchi/toolkit.rb +9 -8
- data/lib/samagotchi/toolkit_command.rb +2 -2
- data/lib/samagotchi/tools/builtins.rb +1 -5
- data/lib/samagotchi/tools/delegate_relay.rb +7 -7
- data/lib/samagotchi/tools/delegate_wait.rb +1 -1
- data/lib/samagotchi/tools/web_fetch.rb +3 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +5 -2
- data/lib/samagotchi/web/public/app.js +21 -58
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/composer.js +9 -5
- data/lib/samagotchi/web/public/ctx.js +84 -11
- data/lib/samagotchi/web/public/index.html +32 -36
- data/lib/samagotchi/web/public/info_bar_model.js +2 -1
- data/lib/samagotchi/web/public/markdown_place.js +53 -0
- data/lib/samagotchi/web/public/question_cards.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +3 -0
- data/lib/samagotchi/web/public/stage_slots.js +3 -3
- data/lib/samagotchi/web/public/stream_handlers.js +38 -3
- data/lib/samagotchi/web/public/timing.js +11 -2
- data/lib/samagotchi/web/public/toasts.js +69 -0
- metadata +8 -2
- data/lib/samagotchi/web/message_parts.rb +0 -274
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class SessionMetrics
|
|
5
|
+
# The sums over a session's finished turn records, kept as running
|
|
6
|
+
# counters (#add folds one record in) and read with the running turn's
|
|
7
|
+
# finished generations folded in (#with_running), so /stats agrees
|
|
8
|
+
# mid-turn. The tool tallies (by tool, aliased, errors) are counted per
|
|
9
|
+
# tool record, not per turn: SessionMetrics keeps those apart.
|
|
10
|
+
class Totals < Data.define(
|
|
11
|
+
:cancellations, :iterations, :gen_ms, :retries, :cuts, :capped,
|
|
12
|
+
:prompt_sum, :completion_sum, :cached_sum, :cache_write_sum, :reprefill_sum, :reasoning_sum,
|
|
13
|
+
:cost_sum, :cost_estimate_sum, :decode_ms_sum, :decode_tokens_sum,
|
|
14
|
+
# The newest speeds a record has, and where the decode one came from.
|
|
15
|
+
:last_decode_tps, :last_prefill_tps, :tps_source,
|
|
16
|
+
# The kinds of counts (server / estimate / mixed) the records came from.
|
|
17
|
+
:token_sources
|
|
18
|
+
)
|
|
19
|
+
# The record keys each sum adds up (a turn record's own names).
|
|
20
|
+
RECORD_SUMS = {
|
|
21
|
+
prompt_sum: :prompt_tokens_sum, completion_sum: :completion_tokens, iterations: :iterations,
|
|
22
|
+
gen_ms: :gen_ms, retries: :retries, cuts: :cuts, capped: :capped, cached_sum: :cached_tokens_sum,
|
|
23
|
+
cache_write_sum: :cache_write_tokens_sum, reprefill_sum: :reprefill_tokens_sum,
|
|
24
|
+
reasoning_sum: :reasoning_tokens, cost_sum: :cost, cost_estimate_sum: :cost_estimate,
|
|
25
|
+
decode_ms_sum: :decode_ms, decode_tokens_sum: :decode_tokens
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# The running turn's (TurnState) counters each sum adds while it runs.
|
|
29
|
+
TURN_SUMS = {
|
|
30
|
+
iterations: :iteration_count, gen_ms: :gen_latency_accum, retries: :retries, cuts: :cuts,
|
|
31
|
+
capped: :capped, prompt_sum: :prompt_sum, completion_sum: :completion_sum, cached_sum: :cached_sum,
|
|
32
|
+
cache_write_sum: :cache_write_sum, reprefill_sum: :reprefill_sum, reasoning_sum: :reasoning_sum,
|
|
33
|
+
cost_sum: :cost_sum, cost_estimate_sum: :cost_estimate_sum, decode_ms_sum: :decode_ms_sum,
|
|
34
|
+
decode_tokens_sum: :decode_tokens_sum
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
def self.zero
|
|
38
|
+
new(cancellations: 0, **RECORD_SUMS.keys.to_h { |key| [key, 0] }, last_decode_tps: nil,
|
|
39
|
+
last_prefill_tps: nil, tps_source: nil, token_sources: [].freeze)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# These totals with one finished turn record (symbol keys, as saved;
|
|
43
|
+
# an older record lacks some counts) counted in.
|
|
44
|
+
# @param record [Hash]
|
|
45
|
+
# @return [Totals]
|
|
46
|
+
def add(record)
|
|
47
|
+
sums = RECORD_SUMS.to_h { |sum, key| [sum, public_send(sum) + number(record[key])] }
|
|
48
|
+
sums[:cancellations] = cancellations + 1 if record[:status] == "canceled"
|
|
49
|
+
# The newest speeds a record has (an older record has none).
|
|
50
|
+
if record[:decode_tps].is_a?(Numeric)
|
|
51
|
+
sums[:last_decode_tps] = record[:decode_tps]
|
|
52
|
+
sums[:tps_source] = record[:tps_source]
|
|
53
|
+
end
|
|
54
|
+
sums[:last_prefill_tps] = record[:prefill_tps] if record[:prefill_tps].is_a?(Numeric)
|
|
55
|
+
source = record[:token_source]
|
|
56
|
+
sums[:token_sources] = (token_sources | [source.to_s]).freeze unless source.nil?
|
|
57
|
+
with(**sums)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# These totals with a later segment's counts and newest speeds.
|
|
61
|
+
# @param later [Totals]
|
|
62
|
+
# @return [Totals]
|
|
63
|
+
def merge(later)
|
|
64
|
+
sums = RECORD_SUMS.keys.to_h { |key| [key, public_send(key) + later.public_send(key)] }
|
|
65
|
+
with(**sums, cancellations: cancellations + later.cancellations,
|
|
66
|
+
token_sources: (token_sources | later.token_sources).freeze,
|
|
67
|
+
last_decode_tps: later.last_decode_tps || last_decode_tps,
|
|
68
|
+
tps_source: later.last_decode_tps ? later.tps_source : tps_source,
|
|
69
|
+
last_prefill_tps: later.last_prefill_tps || last_prefill_tps)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# These totals with the running turn's counters so far (its finished
|
|
73
|
+
# generations, calls and iterations) added; the last speeds are the
|
|
74
|
+
# newest a generation reported. nil: these totals.
|
|
75
|
+
# @param turn [TurnState, nil]
|
|
76
|
+
# @return [Totals]
|
|
77
|
+
def with_running(turn)
|
|
78
|
+
return self unless turn
|
|
79
|
+
|
|
80
|
+
sums = TURN_SUMS.to_h { |sum, key| [sum, public_send(sum) + (turn.public_send(key) || 0)] }
|
|
81
|
+
if turn.last_decode_tps
|
|
82
|
+
sums[:last_decode_tps] = turn.last_decode_tps
|
|
83
|
+
sums[:tps_source] = turn.tps_source
|
|
84
|
+
end
|
|
85
|
+
sums[:last_prefill_tps] = turn.last_prefill_tps if turn.last_prefill_tps
|
|
86
|
+
sums[:token_sources] = (token_sources + turn.token_sources).freeze
|
|
87
|
+
with(**sums)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The snapshot's tokens block.
|
|
91
|
+
# @return [Hash]
|
|
92
|
+
def tokens_block
|
|
93
|
+
{
|
|
94
|
+
prompt_sum: prompt_sum,
|
|
95
|
+
completion_sum: completion_sum,
|
|
96
|
+
cached_sum: cached_sum,
|
|
97
|
+
cache_write_sum: cache_write_sum,
|
|
98
|
+
reprefill_sum: reprefill_sum,
|
|
99
|
+
reasoning_sum: reasoning_sum,
|
|
100
|
+
cost_sum: cost_sum,
|
|
101
|
+
cost_estimate_sum: cost_estimate_sum,
|
|
102
|
+
decode_ms_sum: decode_ms_sum.round,
|
|
103
|
+
decode_tokens_sum: decode_tokens_sum,
|
|
104
|
+
avg_decode_tps: decode_ms_sum.positive? ? (decode_tokens_sum * 1000.0 / decode_ms_sum).round(1) : nil,
|
|
105
|
+
last_decode_tps: last_decode_tps,
|
|
106
|
+
last_prefill_tps: last_prefill_tps,
|
|
107
|
+
tps_source: tps_source,
|
|
108
|
+
source: combined_source
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def number(value)
|
|
115
|
+
value.is_a?(Numeric) ? value : 0
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# server, estimate, or mixed when both kinds of counts (or a turn that
|
|
119
|
+
# was already mixed) are in; nil before any.
|
|
120
|
+
def combined_source
|
|
121
|
+
kinds = token_sources.compact.map(&:to_s)
|
|
122
|
+
.flat_map { |source| source == "mixed" ? %w[server estimate] : [source] }.uniq
|
|
123
|
+
kinds.size > 1 ? "mixed" : kinds.first
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -10,6 +10,8 @@ require_relative "atomic_file"
|
|
|
10
10
|
require_relative "token_usage"
|
|
11
11
|
require_relative "model_price"
|
|
12
12
|
require_relative "session"
|
|
13
|
+
require_relative "session_metrics/totals"
|
|
14
|
+
require_relative "session_metrics/model_segment"
|
|
13
15
|
|
|
14
16
|
module Samagotchi
|
|
15
17
|
# SessionMetrics is a persistent, error-isolated collector of per-session
|
|
@@ -54,8 +56,9 @@ module Samagotchi
|
|
|
54
56
|
GenerationSpeed = Data.define(:decode_tps, :source)
|
|
55
57
|
|
|
56
58
|
# What #finish_generation reports: the generation's speed (nil when it
|
|
57
|
-
# had none)
|
|
58
|
-
|
|
59
|
+
# had none), the session's tokens block and its per-model rows
|
|
60
|
+
# (model_segments) as the snapshot has them.
|
|
61
|
+
GenerationReport = Data.define(:speed, :tokens, :model_segments)
|
|
59
62
|
|
|
60
63
|
# memory_index: the memory indexes the session's prompt held
|
|
61
64
|
# ({ system: { tokens:, lines: }, project: … }, string keys as saved),
|
|
@@ -142,6 +145,13 @@ module Samagotchi
|
|
|
142
145
|
:cuts,
|
|
143
146
|
:capped,
|
|
144
147
|
:model,
|
|
148
|
+
# The model the turn ran on (asked host:model, :generation_started's
|
|
149
|
+
# model_ref; the first generation's), and whether its open generation
|
|
150
|
+
# is the first after a /model switch (its prefill is recorded).
|
|
151
|
+
:model_ref,
|
|
152
|
+
:gen_switch,
|
|
153
|
+
:switch_prefill_tokens,
|
|
154
|
+
:switch_prefill_cost_estimate,
|
|
145
155
|
# A turn with no prompt of its own (:turn_started's +continue+): a
|
|
146
156
|
# step-limit Continue, a reminder's or a wake's turn; its record
|
|
147
157
|
# carries it.
|
|
@@ -280,6 +290,22 @@ module Samagotchi
|
|
|
280
290
|
end
|
|
281
291
|
end
|
|
282
292
|
|
|
293
|
+
# The model ref the last turn that named one ran on (a loaded session's
|
|
294
|
+
# too), nil before any: Engine#session= compares it with the effective
|
|
295
|
+
# model's.
|
|
296
|
+
# @return [String, nil]
|
|
297
|
+
def last_model_ref = @mutex.synchronize { @last_model_ref }
|
|
298
|
+
|
|
299
|
+
# /model's new window (Engine#announce_switched_context!), until the
|
|
300
|
+
# new model's first generation reports one: what the snapshot's context
|
|
301
|
+
# (a page's ctx % on load) counts against.
|
|
302
|
+
def note_context_window(tokens, source)
|
|
303
|
+
@mutex.synchronize do
|
|
304
|
+
@context_window_tokens = tokens
|
|
305
|
+
@context_window_source = source
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
283
309
|
# The memory indexes the session's prompt holds (SystemPrompt#memory_index:
|
|
284
310
|
# { system: { tokens:, lines: }, project: … }), set by the Engine when
|
|
285
311
|
# it builds the prompt it sends; the snapshot and analytics.json carry it.
|
|
@@ -344,6 +370,7 @@ module Samagotchi
|
|
|
344
370
|
reset_generation_tokens
|
|
345
371
|
@turn.gen_price = generation_price(event[:price])
|
|
346
372
|
@turn.gen_open = true
|
|
373
|
+
note_model_ref(event[:model_ref])
|
|
347
374
|
end
|
|
348
375
|
end
|
|
349
376
|
when :generation_completed, :generation_cancelled
|
|
@@ -401,7 +428,8 @@ module Samagotchi
|
|
|
401
428
|
def finish_generation
|
|
402
429
|
@mutex.synchronize do
|
|
403
430
|
speed = close_generation if @turn&.gen_open
|
|
404
|
-
GenerationReport.new(speed: speed, tokens:
|
|
431
|
+
GenerationReport.new(speed: speed, tokens: @totals.with_running(@turn).tokens_block,
|
|
432
|
+
model_segments: ModelSegment.rows(segments_with_running(@turn)))
|
|
405
433
|
end
|
|
406
434
|
end
|
|
407
435
|
|
|
@@ -449,14 +477,17 @@ module Samagotchi
|
|
|
449
477
|
# Caller holds the mutex. +records+: :recent (the live snapshot) or :all
|
|
450
478
|
# (analytics.json).
|
|
451
479
|
def build_snapshot(records_mode)
|
|
452
|
-
totals = @totals
|
|
453
480
|
turn = @turn
|
|
481
|
+
totals = @totals.with_running(turn)
|
|
454
482
|
in_flight = turn ? turn.tool_calls_by_id.values : []
|
|
455
483
|
{
|
|
456
484
|
session_id: @session_id,
|
|
457
485
|
turns: @turn_records.size + (turn ? 1 : 0),
|
|
458
|
-
cancellations: totals
|
|
459
|
-
tokens: tokens_block
|
|
486
|
+
cancellations: totals.cancellations,
|
|
487
|
+
tokens: totals.tokens_block,
|
|
488
|
+
# The tokens, cost and speeds per model, in order (ModelSegment.rows:
|
|
489
|
+
# none with one model); they add up to +tokens+.
|
|
490
|
+
model_segments: ModelSegment.rows(segments_with_running(turn)),
|
|
460
491
|
context: context_block(@turn_records),
|
|
461
492
|
memory_index: @memory_index,
|
|
462
493
|
profile: @profile,
|
|
@@ -464,16 +495,16 @@ module Samagotchi
|
|
|
464
495
|
served_model: @served_model,
|
|
465
496
|
served_model_for: @served_model_for,
|
|
466
497
|
tool_calls_total: @tool_records.size + in_flight.size,
|
|
467
|
-
tool_calls_by_tool: tally_into(
|
|
498
|
+
tool_calls_by_tool: tally_into(@tool_tallies[:by_tool].dup, in_flight),
|
|
468
499
|
# The calls the model made under an alias's name (bash run as
|
|
469
500
|
# execute), by its own spelling; only when there are any.
|
|
470
|
-
**(
|
|
471
|
-
tool_errors:
|
|
472
|
-
iterations_total: totals
|
|
473
|
-
gen_latency_ms:
|
|
474
|
-
retries: totals
|
|
475
|
-
cuts: totals
|
|
476
|
-
capped: totals
|
|
501
|
+
**(@tool_tallies[:aliased].empty? ? {} : { tool_calls_aliased: @tool_tallies[:aliased].dup }),
|
|
502
|
+
tool_errors: @tool_tallies[:errors],
|
|
503
|
+
iterations_total: totals.iterations,
|
|
504
|
+
gen_latency_ms: totals.gen_ms.round,
|
|
505
|
+
retries: totals.retries,
|
|
506
|
+
cuts: totals.cuts,
|
|
507
|
+
capped: totals.capped,
|
|
477
508
|
started_at: @started_at,
|
|
478
509
|
last_activity_at: @last_activity_at,
|
|
479
510
|
# From the session's first start (an earlier process's too), to now.
|
|
@@ -484,31 +515,19 @@ module Samagotchi
|
|
|
484
515
|
}
|
|
485
516
|
end
|
|
486
517
|
|
|
487
|
-
# The
|
|
488
|
-
#
|
|
489
|
-
#
|
|
490
|
-
def
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
reprefill_sum: add.call(:reprefill_sum),
|
|
501
|
-
reasoning_sum: add.call(:reasoning_sum),
|
|
502
|
-
cost_sum: add.call(:cost_sum),
|
|
503
|
-
cost_estimate_sum: add.call(:cost_estimate_sum),
|
|
504
|
-
decode_ms_sum: decode_ms.round,
|
|
505
|
-
decode_tokens_sum: decode_tokens,
|
|
506
|
-
avg_decode_tps: decode_ms.positive? ? (decode_tokens * 1000.0 / decode_ms).round(1) : nil,
|
|
507
|
-
last_decode_tps: speed_turn ? speed_turn.last_decode_tps : totals[:last_decode_tps],
|
|
508
|
-
last_prefill_tps: turn&.last_prefill_tps || totals[:last_prefill_tps],
|
|
509
|
-
tps_source: speed_turn ? speed_turn.tps_source : totals[:tps_source],
|
|
510
|
-
source: combined_source(totals[:token_sources] + (turn&.token_sources || []))
|
|
511
|
-
}
|
|
518
|
+
# The model segments with the running turn in: the last one's when it
|
|
519
|
+
# runs on that model or names none yet, else a new one. Caller holds
|
|
520
|
+
# the mutex.
|
|
521
|
+
def segments_with_running(turn)
|
|
522
|
+
return @segments unless turn
|
|
523
|
+
|
|
524
|
+
last = @segments.last
|
|
525
|
+
ref = turn.model_ref
|
|
526
|
+
if last.nil? || (ref && last.model_ref != ref)
|
|
527
|
+
return @segments + [ModelSegment.empty(ref, turn.started_at).with_running(turn)]
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
@segments[0...-1] + [last.with_running(turn)]
|
|
512
531
|
end
|
|
513
532
|
|
|
514
533
|
# The records a snapshot carries, copied. :all is every record; :recent
|
|
@@ -571,6 +590,8 @@ module Samagotchi
|
|
|
571
590
|
cuts: 0,
|
|
572
591
|
capped: 0,
|
|
573
592
|
model: nil,
|
|
593
|
+
model_ref: nil,
|
|
594
|
+
gen_switch: false,
|
|
574
595
|
continue: event[:continue] ? true : false,
|
|
575
596
|
resumed: event[:resumed] ? true : false,
|
|
576
597
|
id: event[:turn_id] || SecureRandom.uuid,
|
|
@@ -689,6 +710,7 @@ module Samagotchi
|
|
|
689
710
|
def close_generation
|
|
690
711
|
turn = @turn
|
|
691
712
|
turn.gen_open = false
|
|
713
|
+
note_switch_prefill(turn)
|
|
692
714
|
speed = nil
|
|
693
715
|
if turn.gen_had_server
|
|
694
716
|
completion = turn.gen_completion_max
|
|
@@ -712,6 +734,42 @@ module Samagotchi
|
|
|
712
734
|
speed
|
|
713
735
|
end
|
|
714
736
|
|
|
737
|
+
# The turn's model, from its first generation that names one; that
|
|
738
|
+
# generation is a switch's when the last turn that named a model ran on
|
|
739
|
+
# another (a turn with none, stopped before it asked, never counts; nor
|
|
740
|
+
# does a legacy session's first named one). Caller holds the mutex.
|
|
741
|
+
def note_model_ref(ref)
|
|
742
|
+
return if @turn.model_ref || ref.to_s.empty?
|
|
743
|
+
|
|
744
|
+
@turn.model_ref = ref.to_s
|
|
745
|
+
@turn.gen_switch = !@last_model_ref.nil? && @last_model_ref != @turn.model_ref
|
|
746
|
+
@last_model_ref = @turn.model_ref
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
# The first generation after a switch prefilled the history on the new
|
|
750
|
+
# model: its uncached prompt (the whole prompt less the cached reads;
|
|
751
|
+
# llama.cpp's native tokens_evaluated is the whole prompt too), with its
|
|
752
|
+
# cost estimated from the configured price, never a completion's. None
|
|
753
|
+
# for a generation with estimated tokens only or a prefill too small to
|
|
754
|
+
# be one (REPREFILL_MIN_TOKENS). Caller holds the mutex.
|
|
755
|
+
def note_switch_prefill(turn)
|
|
756
|
+
return unless turn.gen_switch
|
|
757
|
+
|
|
758
|
+
turn.gen_switch = false
|
|
759
|
+
return unless turn.gen_had_server
|
|
760
|
+
|
|
761
|
+
tokens = [turn.gen_prompt_max - turn.gen_cached_max, 0].max
|
|
762
|
+
return if tokens < TokenUsage::REPREFILL_MIN_TOKENS
|
|
763
|
+
|
|
764
|
+
turn.switch_prefill_tokens = tokens
|
|
765
|
+
return unless turn.gen_price
|
|
766
|
+
|
|
767
|
+
turn.switch_prefill_cost_estimate = turn.gen_price.cost(prompt_tokens: turn.gen_prompt_max,
|
|
768
|
+
cached_tokens: turn.gen_cached_max,
|
|
769
|
+
cache_write_tokens: turn.gen_cache_write_max,
|
|
770
|
+
completion_tokens: 0)
|
|
771
|
+
end
|
|
772
|
+
|
|
715
773
|
# The generation's cached/reasoning counts, cost and decode speed, into
|
|
716
774
|
# the turn. The speed is the server's (llama.cpp's predicted_per_second
|
|
717
775
|
# over its predicted_ms) or, without one, an estimate: the completion
|
|
@@ -781,6 +839,7 @@ module Samagotchi
|
|
|
781
839
|
sources = turn.token_sources
|
|
782
840
|
{
|
|
783
841
|
model: turn.model,
|
|
842
|
+
model_ref: turn.model_ref,
|
|
784
843
|
generations: turn.generations,
|
|
785
844
|
prompt_tokens: turn.prompt_last,
|
|
786
845
|
prompt_tokens_max: turn.prompt_max,
|
|
@@ -815,6 +874,8 @@ module Samagotchi
|
|
|
815
874
|
fields[:reprefill_tokens_sum] = turn.reprefill_sum if turn.reprefill_sum.positive?
|
|
816
875
|
fields.merge!(cost: turn.cost_sum, cost_source: "reported") if turn.cost_sum
|
|
817
876
|
fields[:cost_estimate] = turn.cost_estimate_sum if turn.cost_estimate_sum.positive?
|
|
877
|
+
fields[:switch_prefill_tokens] = turn.switch_prefill_tokens if turn.switch_prefill_tokens
|
|
878
|
+
fields[:switch_prefill_cost_estimate] = turn.switch_prefill_cost_estimate if turn.switch_prefill_cost_estimate
|
|
818
879
|
fields
|
|
819
880
|
end
|
|
820
881
|
|
|
@@ -1017,6 +1078,7 @@ module Samagotchi
|
|
|
1017
1078
|
reset_totals
|
|
1018
1079
|
@turn_records.each { |record| count_turn(record) }
|
|
1019
1080
|
@tool_records.each { |record| count_tool(record) }
|
|
1081
|
+
@last_model_ref = @turn.model_ref if @turn&.model_ref
|
|
1020
1082
|
@started_at = earliest_timestamp(prior["started_at"], @started_at)
|
|
1021
1083
|
@last_activity_at ||= prior["last_activity_at"]
|
|
1022
1084
|
# The served model last reported, with the name asked for then (the
|
|
@@ -1057,48 +1119,32 @@ module Samagotchi
|
|
|
1057
1119
|
end
|
|
1058
1120
|
|
|
1059
1121
|
# The session totals over the finished records, as running counters
|
|
1060
|
-
# (#snapshot adds the running turn)
|
|
1122
|
+
# (#snapshot adds the running turn), and the tool tallies over the tool
|
|
1123
|
+
# records. Caller holds the mutex.
|
|
1061
1124
|
def reset_totals
|
|
1062
|
-
@totals =
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
last_prefill_tps: nil, tps_source: nil }
|
|
1125
|
+
@totals = Totals.zero
|
|
1126
|
+
@segments = []
|
|
1127
|
+
@last_model_ref = nil
|
|
1128
|
+
@tool_tallies = { by_tool: {}, aliased: {}, errors: 0 }
|
|
1067
1129
|
end
|
|
1068
1130
|
|
|
1069
1131
|
def count_turn(record)
|
|
1070
|
-
totals = @totals
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
totals[:gen_ms] += number(record[:gen_ms])
|
|
1076
|
-
totals[:retries] += number(record[:retries])
|
|
1077
|
-
totals[:cuts] += number(record[:cuts])
|
|
1078
|
-
totals[:capped] += number(record[:capped])
|
|
1079
|
-
totals[:cached_sum] += number(record[:cached_tokens_sum])
|
|
1080
|
-
totals[:cache_write_sum] += number(record[:cache_write_tokens_sum])
|
|
1081
|
-
totals[:reprefill_sum] += number(record[:reprefill_tokens_sum])
|
|
1082
|
-
totals[:reasoning_sum] += number(record[:reasoning_tokens])
|
|
1083
|
-
totals[:cost_sum] += number(record[:cost])
|
|
1084
|
-
totals[:cost_estimate_sum] += number(record[:cost_estimate])
|
|
1085
|
-
totals[:decode_ms_sum] += number(record[:decode_ms])
|
|
1086
|
-
totals[:decode_tokens_sum] += number(record[:decode_tokens])
|
|
1087
|
-
# The newest speeds a record has (an older record has none).
|
|
1088
|
-
if record[:decode_tps].is_a?(Numeric)
|
|
1089
|
-
totals[:last_decode_tps] = record[:decode_tps]
|
|
1090
|
-
totals[:tps_source] = record[:tps_source]
|
|
1132
|
+
@totals = @totals.add(record)
|
|
1133
|
+
if ModelSegment.splits?(@segments.last, record)
|
|
1134
|
+
@segments << ModelSegment.open(record)
|
|
1135
|
+
else
|
|
1136
|
+
@segments[-1] = @segments.last.add(record)
|
|
1091
1137
|
end
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1138
|
+
# The model the last turn that named one ran on: what a switch is
|
|
1139
|
+
# told by (#note_model_ref).
|
|
1140
|
+
@last_model_ref = record[:model_ref].to_s unless record[:model_ref].to_s.empty?
|
|
1095
1141
|
end
|
|
1096
1142
|
|
|
1097
1143
|
def count_tool(record)
|
|
1098
|
-
@
|
|
1099
|
-
tally_into(@
|
|
1144
|
+
@tool_tallies[:errors] += 1 if record[:status] == "error"
|
|
1145
|
+
tally_into(@tool_tallies[:by_tool], [record])
|
|
1100
1146
|
called_as = record[:called_as].to_s
|
|
1101
|
-
@
|
|
1147
|
+
@tool_tallies[:aliased][called_as] = @tool_tallies[:aliased].fetch(called_as, 0) + 1 unless called_as.empty?
|
|
1102
1148
|
end
|
|
1103
1149
|
|
|
1104
1150
|
def tally_into(counts, tools)
|
|
@@ -1109,17 +1155,6 @@ module Samagotchi
|
|
|
1109
1155
|
counts
|
|
1110
1156
|
end
|
|
1111
1157
|
|
|
1112
|
-
def number(value)
|
|
1113
|
-
value.is_a?(Numeric) ? value : 0
|
|
1114
|
-
end
|
|
1115
|
-
|
|
1116
|
-
# server, estimate, or mixed when both kinds of counts (or a turn that
|
|
1117
|
-
# was already mixed) are in; nil before any.
|
|
1118
|
-
def combined_source(sources)
|
|
1119
|
-
kinds = sources.compact.map(&:to_s).flat_map { |source| source == "mixed" ? %w[server estimate] : [source] }.uniq
|
|
1120
|
-
kinds.size > 1 ? "mixed" : kinds.first
|
|
1121
|
-
end
|
|
1122
|
-
|
|
1123
1158
|
def earliest_timestamp(*timestamps)
|
|
1124
1159
|
timestamps.compact.min_by { |value| Time.iso8601(value.to_s) }
|
|
1125
1160
|
rescue ArgumentError
|
|
@@ -88,9 +88,10 @@ module Samagotchi
|
|
|
88
88
|
# A question left waiting (exit 3), in full on stderr as `chi send
|
|
89
89
|
# --wait` prints it (ParentReport), so a script or a parent agent
|
|
90
90
|
# can answer it with chi answer.
|
|
91
|
-
# @param pending [Hash] the pending question
|
|
91
|
+
# @param pending [Hash] the pending question as the Bridge sent it
|
|
92
92
|
def report_unanswered(pending, session_id:, err: $stderr)
|
|
93
|
-
|
|
93
|
+
question = PendingQuestion.from_file(pending)
|
|
94
|
+
err.print("chi: #{ParentReport.question_text(question, session_id: session_id)}")
|
|
94
95
|
err.flush
|
|
95
96
|
end
|
|
96
97
|
|
|
@@ -28,7 +28,7 @@ require_relative "../children_status"
|
|
|
28
28
|
require_relative "../guardrails/parent_approvals"
|
|
29
29
|
require_relative "../tool_activity"
|
|
30
30
|
require_relative "../tool_view"
|
|
31
|
-
require_relative "../
|
|
31
|
+
require_relative "../message_parts"
|
|
32
32
|
|
|
33
33
|
module Samagotchi
|
|
34
34
|
class TerminalUI
|
|
@@ -1274,7 +1274,7 @@ module Samagotchi
|
|
|
1274
1274
|
@screen.commit(format_steer_line(source: m[:source], text: m[:content]))
|
|
1275
1275
|
elsif %w[model assistant].include?(m[:role].to_s)
|
|
1276
1276
|
responses = messages.drop(i + 1).take_while { |r| r[:role].to_s == "tool_response" }
|
|
1277
|
-
Array(
|
|
1277
|
+
Array(MessageParts.for_message(m, responses)&.dig(:tools)).each do |part|
|
|
1278
1278
|
record = join_record(records, part)
|
|
1279
1279
|
duration = record&.dig("duration_ms")
|
|
1280
1280
|
@screen.commit(join_tool_line(part, duration_ms: duration.is_a?(Numeric) ? duration : nil,
|
|
@@ -9,6 +9,7 @@ require_relative "../steer_cut"
|
|
|
9
9
|
require_relative "../memory_bundle/index_size"
|
|
10
10
|
require_relative "../prompt_note"
|
|
11
11
|
require_relative "../prompt_files"
|
|
12
|
+
require_relative "model_segments_text"
|
|
12
13
|
|
|
13
14
|
module Samagotchi
|
|
14
15
|
class TerminalUI
|
|
@@ -16,6 +17,8 @@ module Samagotchi
|
|
|
16
17
|
# `tool>` line, elapsed durations. Pure apart from reading whether
|
|
17
18
|
# $stdout is a colour terminal.
|
|
18
19
|
module Formatting
|
|
20
|
+
include ModelSegmentsText
|
|
21
|
+
|
|
19
22
|
# The /stats text for a metrics snapshot, for the REPL and attached
|
|
20
23
|
# views (an instance includes the module) and for `chi sessions stats`
|
|
21
24
|
# (no UI: the same text from the CLI).
|
|
@@ -435,13 +438,18 @@ module Samagotchi
|
|
|
435
438
|
# /stats' cost: the reported part and the estimated one, each when present.
|
|
436
439
|
# +price_host+ names the host whose hosts.<name>.models prices the
|
|
437
440
|
# estimate came from (Engine#stats_snapshot; nil from an older worker).
|
|
438
|
-
|
|
441
|
+
# +per_model+: the session ran on several models (model_segments), each
|
|
442
|
+
# priced from its own host's prices.
|
|
443
|
+
def stats_cost_text(tokens, price_host: nil, per_model: false)
|
|
439
444
|
reported = cost_text(tokens[:cost_sum])
|
|
440
445
|
estimated = cost_text(tokens[:cost_estimate_sum], estimate: true)
|
|
441
446
|
return nil if reported.empty? && estimated.empty?
|
|
442
447
|
return "#{reported} (this session only)" if estimated.empty?
|
|
443
448
|
|
|
444
|
-
source =
|
|
449
|
+
source = if per_model then "each model's host prices"
|
|
450
|
+
elsif price_host.to_s.empty? then "configured prices"
|
|
451
|
+
else "hosts.#{price_host}.models prices"
|
|
452
|
+
end
|
|
445
453
|
estimate = "#{estimated} from #{source}"
|
|
446
454
|
"#{reported.empty? ? estimate : "#{reported} reported + #{estimate}"} (this session only)"
|
|
447
455
|
end
|
|
@@ -574,12 +582,17 @@ module Samagotchi
|
|
|
574
582
|
lines << " called as: #{aliased.map { |k, v| "#{k}=#{v}" }.join(", ")}"
|
|
575
583
|
end
|
|
576
584
|
lines << "iterations: #{snapshot[:iterations_total]}"
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
585
|
+
segments = Array(snapshot[:model_segments])
|
|
586
|
+
if segments.size >= 2
|
|
587
|
+
lines.concat(models_block_lines(segments, snapshot[:turns], tokens, token_src_label))
|
|
588
|
+
else
|
|
589
|
+
# Summed over every request: each prompt is sent in full again.
|
|
590
|
+
lines << "tokens in/out: #{tokens[:prompt_sum].to_i}/#{tokens[:completion_sum].to_i} (all requests, #{token_src_label})" \
|
|
591
|
+
"#{token_breakdown_text(tokens)}"
|
|
592
|
+
end
|
|
593
|
+
speed = stats_speed_text(tokens, last_model: segments.size >= 2 ? last_speed_label(segments) : nil)
|
|
581
594
|
lines << "speed: #{speed}" if speed
|
|
582
|
-
cost = stats_cost_text(tokens, price_host: snapshot[:price_host])
|
|
595
|
+
cost = stats_cost_text(tokens, price_host: snapshot[:price_host], per_model: segments.size >= 2)
|
|
583
596
|
lines << "cost: #{cost}" if cost
|
|
584
597
|
lines << "gen latency (ms): #{snapshot[:gen_latency_ms]}"
|
|
585
598
|
lines << "cancellations: #{snapshot[:cancellations]}"
|
|
@@ -682,15 +695,16 @@ module Samagotchi
|
|
|
682
695
|
|
|
683
696
|
# "87 tok/s out, 1.9k tok/s prompt (last, server), avg 81 tok/s", or nil
|
|
684
697
|
# before a generation had a speed. The prompt (prefill) speed only when
|
|
685
|
-
# the server reported it.
|
|
686
|
-
|
|
698
|
+
# the server reported it. +last_model+: the model the last speed came
|
|
699
|
+
# from, named when the session ran on several.
|
|
700
|
+
def stats_speed_text(tokens, last_model: nil)
|
|
687
701
|
last = tokens[:last_decode_tps]
|
|
688
702
|
return nil unless last
|
|
689
703
|
|
|
690
704
|
source = tokens[:tps_source].to_s
|
|
691
705
|
text = "#{speed_text(last, source)} out"
|
|
692
706
|
text << ", #{speed_text(tokens[:last_prefill_tps], "server")} prompt" if tokens[:last_prefill_tps]
|
|
693
|
-
text << " (last, #{source})"
|
|
707
|
+
text << " (last, #{source}#{", #{last_model}" if last_model})"
|
|
694
708
|
text << ", avg #{speed_text(tokens[:avg_decode_tps], source)}" if tokens[:avg_decode_tps]
|
|
695
709
|
text
|
|
696
710
|
end
|