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,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "../memory_bundle/index_size"
|
|
5
|
+
|
|
6
|
+
module Samagotchi
|
|
7
|
+
class TerminalUI
|
|
8
|
+
# A session's rows per model in /stats (the snapshot's model_segments).
|
|
9
|
+
# The four public model_segment_* labels are the words the web's ctx
|
|
10
|
+
# tooltip shares (ctx.js; spec/shared/labels_matrix.json model_segments).
|
|
11
|
+
# Rows come symbol- or string-keyed (the attached TUI's as JSON).
|
|
12
|
+
# Included by Formatting, whose cost_text and speed_text it uses.
|
|
13
|
+
module ModelSegmentsText
|
|
14
|
+
# A row's model: its ref; the rows folded past the cap ("3 earlier
|
|
15
|
+
# segments", told by folded_segments); a session's turns from before
|
|
16
|
+
# turns named their model ("earlier turns").
|
|
17
|
+
def model_segment_label(row)
|
|
18
|
+
folded = segment_value(row, :folded_segments).to_i
|
|
19
|
+
return "#{folded} earlier segments" if folded.positive?
|
|
20
|
+
|
|
21
|
+
ref = segment_value(row, :model_ref).to_s
|
|
22
|
+
ref.empty? ? "earlier turns" : ref
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# "6.0k in / 40 out": the row's prompt and completion sums.
|
|
26
|
+
def model_segment_tokens_text(row)
|
|
27
|
+
count = ->(key) { MemoryBundle::IndexSize.count_text(segment_value(row, key).to_i) }
|
|
28
|
+
"#{count.call(:prompt_sum)} in / #{count.call(:completion_sum)} out"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# "$0.42", "~$0.12", "$0.42 + ~$0.12" (reported + estimated); "" for none.
|
|
32
|
+
def model_segment_cost_text(row)
|
|
33
|
+
[cost_text(segment_value(row, :cost_sum)), cost_text(segment_value(row, :cost_estimate_sum), estimate: true)]
|
|
34
|
+
.reject(&:empty?).join(" + ")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# "switch prefill 3.0k uncached prompt ≈$0.0060": what the switch to the
|
|
38
|
+
# row's model prefilled (its cost an estimate from the configured
|
|
39
|
+
# price, left out without one); "" for none.
|
|
40
|
+
def model_segment_prefill_text(row)
|
|
41
|
+
tokens = segment_value(row, :prefill_tokens).to_i
|
|
42
|
+
return "" unless tokens.positive?
|
|
43
|
+
|
|
44
|
+
cost = cost_text(segment_value(row, :prefill_cost_estimate))
|
|
45
|
+
"switch prefill #{MemoryBundle::IndexSize.count_text(tokens)} uncached prompt#{" ≈#{cost}" unless cost.empty?}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def segment_value(row, key)
|
|
51
|
+
row[key] || row[key.to_s]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# /stats' models: block, in place of the tokens line: a row per
|
|
55
|
+
# segment (model, when, turns, tokens, its average speed and cost), a
|
|
56
|
+
# switch's prefill under the row of the model switched to, and the
|
|
57
|
+
# total (the tokens line's sums and breakdown).
|
|
58
|
+
def models_block_lines(segments, turns, tokens, token_src_label)
|
|
59
|
+
dated = segment_dates_differ?(segments)
|
|
60
|
+
cells = segments.map do |row|
|
|
61
|
+
speed = speed_text(segment_value(row, :avg_decode_tps), segment_value(row, :tps_source))
|
|
62
|
+
[model_segment_label(row), segment_span_text(row, dated: dated), turns_text(segment_value(row, :turns)),
|
|
63
|
+
[model_segment_tokens_text(row), speed, model_segment_cost_text(row)].reject(&:empty?).join(" ")]
|
|
64
|
+
end
|
|
65
|
+
total = ["total", "", turns_text(turns),
|
|
66
|
+
"#{MemoryBundle::IndexSize.count_text(tokens[:prompt_sum].to_i)} in / " \
|
|
67
|
+
"#{MemoryBundle::IndexSize.count_text(tokens[:completion_sum].to_i)} out " \
|
|
68
|
+
"(all requests, #{token_src_label}#{token_breakdown_text(tokens)})"]
|
|
69
|
+
widths = (0..2).map { |i| (cells + [total]).map { |cell| cell[i].length }.max }
|
|
70
|
+
row_text = ->(cell) { cell.each_with_index.map { |text, i| i < 3 ? text.ljust(widths[i]) : text }.join(" ") }
|
|
71
|
+
lines = []
|
|
72
|
+
segments.each_with_index do |row, i|
|
|
73
|
+
lines << "#{i.zero? ? "models:".ljust(18) : " " * 18}#{row_text.call(cells[i])}"
|
|
74
|
+
prefill = model_segment_prefill_text(row)
|
|
75
|
+
lines << "#{" " * 20}└ #{prefill}" unless prefill.empty?
|
|
76
|
+
end
|
|
77
|
+
lines << "#{" " * 18}#{row_text.call(total)}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def turns_text(count)
|
|
81
|
+
"#{count.to_i} #{count.to_i == 1 ? "turn" : "turns"}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# "10:02–10:41", "10:43–now" (a row with a turn running); with the
|
|
85
|
+
# day ("Oct 16 10:02") when the rows span more than one day.
|
|
86
|
+
def segment_span_text(row, dated:)
|
|
87
|
+
started = segment_time(segment_value(row, :started_at))
|
|
88
|
+
ended = segment_time(segment_value(row, :ended_at))
|
|
89
|
+
format_time = lambda { |time|
|
|
90
|
+
if time
|
|
91
|
+
time.strftime(dated ? "%b %-d %H:%M" : "%H:%M")
|
|
92
|
+
else
|
|
93
|
+
"?"
|
|
94
|
+
end
|
|
95
|
+
}
|
|
96
|
+
"#{format_time.call(started)}–#{ended ? format_time.call(ended) : "now"}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def segment_dates_differ?(segments)
|
|
100
|
+
days = segments.flat_map { |row| [segment_value(row, :started_at), segment_value(row, :ended_at)] }
|
|
101
|
+
.filter_map { |stamp| segment_time(stamp)&.strftime("%F") }
|
|
102
|
+
days.uniq.size > 1
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def segment_time(stamp)
|
|
106
|
+
stamp ? Time.iso8601(stamp.to_s).localtime : nil
|
|
107
|
+
rescue ArgumentError
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The row the session's last speed came from: the newest with one.
|
|
112
|
+
def last_speed_label(segments)
|
|
113
|
+
row = segments.reverse_each.find { |segment| segment_value(segment, :last_decode_tps) }
|
|
114
|
+
row && model_segment_label(row)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -89,7 +89,9 @@ module Samagotchi
|
|
|
89
89
|
# hint). Other events change nothing.
|
|
90
90
|
def take_event(event)
|
|
91
91
|
case event[:type]
|
|
92
|
-
|
|
92
|
+
# usage nil (a /model whose window didn't resolve) clears it.
|
|
93
|
+
when :context_status
|
|
94
|
+
update(context: event[:usage] ? { est_pct: event.dig(:usage, :estimated_pct), bucket: event[:bucket] } : nil)
|
|
93
95
|
when :used_memories_updated then update(used_memories: Array(event[:used_memory_names]))
|
|
94
96
|
when :generation_completed
|
|
95
97
|
update(served: [event[:served_model], event[:requested_model], event[:served_expected_by]]) if event[:served_model]
|
|
@@ -873,6 +873,7 @@ module Samagotchi
|
|
|
873
873
|
end
|
|
874
874
|
if command
|
|
875
875
|
show_command_result(command)
|
|
876
|
+
refresh_status_row(clear_unknown_context: true) if command.changed.include?(:model)
|
|
876
877
|
persist_recent_history(input) if command.shell
|
|
877
878
|
return
|
|
878
879
|
end
|
|
@@ -1400,9 +1401,10 @@ module Samagotchi
|
|
|
1400
1401
|
|
|
1401
1402
|
# The status row under the prompt, from the Engine's state (what a
|
|
1402
1403
|
# worker hands an attached TUI when it joins): drawn when it changed.
|
|
1403
|
-
def refresh_status_row
|
|
1404
|
+
def refresh_status_row(clear_unknown_context: false)
|
|
1404
1405
|
state = @engine.session_state_snapshot.merge(model_name: @effective_model_name)
|
|
1405
1406
|
@status_row.take_state(state, default_model: @default_model_name)
|
|
1407
|
+
@status_row.update(context: nil) if clear_unknown_context && state.key?(:context_status) && state[:context_status].nil?
|
|
1406
1408
|
refresh_children_status
|
|
1407
1409
|
end
|
|
1408
1410
|
|
|
@@ -1463,13 +1465,13 @@ module Samagotchi
|
|
|
1463
1465
|
def drain_pending_question?
|
|
1464
1466
|
pq = @pending_question_event
|
|
1465
1467
|
# Also check Engine's persisted pending (covers resume)
|
|
1466
|
-
pq ||= @engine.pending_question
|
|
1468
|
+
pq ||= @engine.pending_question&.to_h
|
|
1467
1469
|
return false unless pq
|
|
1468
1470
|
|
|
1469
1471
|
# If the pending no longer matches the engine's current pending (stale
|
|
1470
1472
|
# event after sync handler already answered and cleared), discard.
|
|
1471
1473
|
current = @engine.pending_question
|
|
1472
|
-
if current && (pq[:id] || pq["id"]).to_s !=
|
|
1474
|
+
if current && (pq[:id] || pq["id"]).to_s != current.id.to_s
|
|
1473
1475
|
@pending_question_event = nil
|
|
1474
1476
|
return false
|
|
1475
1477
|
end
|
|
@@ -38,7 +38,7 @@ module Samagotchi
|
|
|
38
38
|
HEAD_CUT = /\[cut: \d+ of \d+ chars; read it in parts\]\z/
|
|
39
39
|
|
|
40
40
|
# What a web row keeps of a tool's output, on a reload
|
|
41
|
-
# (
|
|
41
|
+
# (MessageParts) and a mid-turn join (Bridge::TurnAccumulator): the
|
|
42
42
|
# row shows 300 chars and the rest on hover. A longer one keeps its head
|
|
43
43
|
# and tail (preview).
|
|
44
44
|
WEB_PREVIEW_CHARS = 2000
|
|
@@ -29,7 +29,7 @@ module Samagotchi
|
|
|
29
29
|
# Runs +calls+ in order (ToolRunner#run each), yielding each run and its
|
|
30
30
|
# index as it finishes. +emit+ takes the dispatch events, the runner's
|
|
31
31
|
# own go to +on_stream_event+.
|
|
32
|
-
# @return [Array<
|
|
32
|
+
# @return [Array<ToolRun>] the runs
|
|
33
33
|
def run_batch(runner, calls, iteration:, emit:, on_stream_event:, cap:)
|
|
34
34
|
emit.call({ type: :tool_dispatch_started, iteration: iteration, call_count: calls.length })
|
|
35
35
|
runs = calls.each_with_index.map do |call, index|
|
|
@@ -45,7 +45,7 @@ module Samagotchi
|
|
|
45
45
|
# The runs' activity, for the turn's tool list (none for a call whose
|
|
46
46
|
# dispatcher raised).
|
|
47
47
|
def activities(runs)
|
|
48
|
-
runs.filter_map
|
|
48
|
+
runs.filter_map(&:activity)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# The native loop's entry for a batch: the capped outputs joined, every image
|
|
@@ -53,14 +53,14 @@ module Samagotchi
|
|
|
53
53
|
# each on its own tool row), and the saved-only fields one per call:
|
|
54
54
|
# +ids+ (ToolIds.next_ids) as tool_ids.
|
|
55
55
|
def joined(runs, ids:)
|
|
56
|
-
images = runs.flat_map { |run| Array(run
|
|
57
|
-
entry = { role: "tool_response", content: runs.map
|
|
56
|
+
images = runs.flat_map { |run| Array(run.images) }
|
|
57
|
+
entry = { role: "tool_response", content: runs.map(&:capped_output).join(SEPARATOR) }
|
|
58
58
|
unless images.empty?
|
|
59
59
|
entry[:images] = images
|
|
60
|
-
entry[:image_counts] = runs.map { |run| Array(run
|
|
60
|
+
entry[:image_counts] = runs.map { |run| Array(run.images).size }
|
|
61
61
|
end
|
|
62
|
-
saved = { tool_params: runs.map
|
|
63
|
-
tool_diffs: runs.map
|
|
62
|
+
saved = { tool_params: runs.map(&:shown_params), tool_labels: runs.map(&:shown_label),
|
|
63
|
+
tool_diffs: runs.map(&:diff), tool_refs: runs.map(&:refs) }
|
|
64
64
|
saved.each { |key, values| entry[key] = values if values.any? }
|
|
65
65
|
entry[:tool_ids] = ids
|
|
66
66
|
entry
|
|
@@ -70,12 +70,12 @@ module Samagotchi
|
|
|
70
70
|
# call by +tool_call_id+, and its chi-owned id (a one-id +ids+, as
|
|
71
71
|
# #joined's tool_ids).
|
|
72
72
|
def single(run, tool_call_id:, ids:)
|
|
73
|
-
entry = { role: "tool_response", content: run
|
|
74
|
-
entry[:images] = run
|
|
75
|
-
entry[:tool_params] = run
|
|
76
|
-
entry[:tool_labels] = run
|
|
77
|
-
entry[:tool_diffs] = run
|
|
78
|
-
entry[:tool_refs] = run
|
|
73
|
+
entry = { role: "tool_response", content: run.capped_output, tool_call_id: tool_call_id }
|
|
74
|
+
entry[:images] = run.images if run.images&.any?
|
|
75
|
+
entry[:tool_params] = run.shown_params if run.shown_params
|
|
76
|
+
entry[:tool_labels] = run.shown_label if run.shown_label
|
|
77
|
+
entry[:tool_diffs] = run.diff if run.diff
|
|
78
|
+
entry[:tool_refs] = run.refs if run.refs
|
|
79
79
|
entry[:tool_ids] = ids
|
|
80
80
|
entry
|
|
81
81
|
end
|
|
@@ -7,7 +7,7 @@ require_relative "file_ref"
|
|
|
7
7
|
module Samagotchi
|
|
8
8
|
# The fields a tool row shows beyond its params, built from the call in
|
|
9
9
|
# one place for every path: the live tool_call_started (ToolRunner#run)
|
|
10
|
-
# and a reload from the saved call (
|
|
10
|
+
# and a reload from the saved call (MessageParts#tool_part). The
|
|
11
11
|
# bridge's snapshot and replay (Bridge::TurnAccumulator) copy them by
|
|
12
12
|
# these lists, so a new field is added here and travels everywhere.
|
|
13
13
|
#
|
|
@@ -15,6 +15,17 @@ require_relative "memory_bundle/index_sync"
|
|
|
15
15
|
require_relative "memory_bundle/index_size"
|
|
16
16
|
|
|
17
17
|
module Samagotchi
|
|
18
|
+
# One call's result from ToolRunner#run, read by ToolResponse (the loops'
|
|
19
|
+
# tool_response entries) and the loops' activity list. Fields a call didn't
|
|
20
|
+
# produce (images, shown_params, shown_label, refs, diff, output_file) are nil.
|
|
21
|
+
ToolRun = Data.define(:output, :capped_output, :truncated, :activity, :images, :shown_params, :shown_label, :refs, :diff,
|
|
22
|
+
:output_file) do
|
|
23
|
+
def initialize(output: nil, capped_output: nil, truncated: nil, activity: nil, images: nil, shown_params: nil,
|
|
24
|
+
shown_label: nil, refs: nil, diff: nil, output_file: nil)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
18
29
|
# The single per-call path both loops use: the tool_call_started and
|
|
19
30
|
# tool_call_completed events, the guardrail gate (before_tool_call hooks
|
|
20
31
|
# and their veto), dispatch through KernelLoop, the after_tool_call hook
|
|
@@ -40,7 +51,7 @@ module Samagotchi
|
|
|
40
51
|
end
|
|
41
52
|
|
|
42
53
|
# @param call_index [Integer] 1-based position of the call in its batch
|
|
43
|
-
# @return [
|
|
54
|
+
# @return [ToolRun] output:, capped_output:, truncated:, activity:,
|
|
44
55
|
# output_file: (the saved whole output of a cut execute result),
|
|
45
56
|
# images: (refs) when the tool returned images the model gets to see, and
|
|
46
57
|
# shown_params: the params line the live row showed, and shown_label:
|
|
@@ -135,14 +146,10 @@ module Samagotchi
|
|
|
135
146
|
fire(:after_tool_call, { type: :after_tool_call, iteration: iteration, tool: call[:name], output: capped,
|
|
136
147
|
status: status || "error", call: call.dup, elapsed_ms: elapsed_ms, result: tool_text })
|
|
137
148
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
run[:refs] = refs unless refs.empty?
|
|
143
|
-
run[:diff] = diff if diff
|
|
144
|
-
run[:output_file] = output_file if output_file
|
|
145
|
-
run
|
|
149
|
+
ToolRun.new(output: output, capped_output: capped, truncated: truncated, activity: result[:activity],
|
|
150
|
+
images: images&.any? ? images : nil,
|
|
151
|
+
shown_params: params && plugin_tool?(call[:name]) ? params : nil,
|
|
152
|
+
shown_label: label, refs: refs.empty? ? nil : refs, diff: diff, output_file: output_file)
|
|
146
153
|
end
|
|
147
154
|
|
|
148
155
|
private
|
data/lib/samagotchi/toolkit.rb
CHANGED
|
@@ -77,16 +77,17 @@ module Samagotchi
|
|
|
77
77
|
super
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
# What the prompt carries
|
|
81
|
-
# and removed ones (the not-loaded line, find_tools'
|
|
82
|
-
# either list has any the kit name the line gives
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def prompt_key
|
|
86
|
-
[declared.map(&:name), reachable, removed, reachable.empty? && removed.empty? ? nil : resolved.name,
|
|
87
|
-
resolved.prompt]
|
|
80
|
+
# What the prompt carries before the kit's prompt text: the declared
|
|
81
|
+
# names, reachable and removed ones (the not-loaded line, find_tools'
|
|
82
|
+
# chi line), and, when either list has any, the kit name the line gives.
|
|
83
|
+
def tools_key
|
|
84
|
+
[declared.map(&:name), reachable, removed, reachable.empty? && removed.empty? ? nil : resolved.name]
|
|
88
85
|
end
|
|
89
86
|
|
|
87
|
+
# What the prompt carries of it, including the kit's prompt text. Two
|
|
88
|
+
# equal keys build the same prompt (/toolkit's "no change", its cost).
|
|
89
|
+
def prompt_key = [*tools_key, resolved.prompt]
|
|
90
|
+
|
|
90
91
|
# What the UIs show (the snapshot, command_ran): nil when it declares
|
|
91
92
|
# every tool on full (full, full +delegate), which has no chip.
|
|
92
93
|
# +left_out+ is +reachable+ and +removed+ together; +prompt+ (the
|
|
@@ -129,9 +129,9 @@ module Samagotchi
|
|
|
129
129
|
|
|
130
130
|
# What the confirm says is read again: from the tools on (the front of
|
|
131
131
|
# every request), or from the kit's prompt text on when only that
|
|
132
|
-
# changes (Explained#
|
|
132
|
+
# changes (Explained#tools_key is unchanged: only a kit's prompt text).
|
|
133
133
|
def reread_line(before, after)
|
|
134
|
-
if before.
|
|
134
|
+
if before.tools_key == after.tools_key
|
|
135
135
|
"The kit's prompt text is in the system prompt: the prompt is read again from it on."
|
|
136
136
|
else
|
|
137
137
|
"The tools are the front of every request: the whole prompt is read again from them on."
|
|
@@ -144,11 +144,7 @@ module Samagotchi
|
|
|
144
144
|
|
|
145
145
|
# The XDG dirs set in +env+, made absolute (nothing for an unset one).
|
|
146
146
|
def xdg_absolute(env = ENV)
|
|
147
|
-
|
|
148
|
-
dirs = {}
|
|
149
|
-
dirs["XDG_STATE_HOME"] = Paths.state_home(env: env) if set.call("XDG_STATE_HOME")
|
|
150
|
-
dirs["XDG_CONFIG_HOME"] = Paths.config_home(env: env) if set.call("XDG_CONFIG_HOME")
|
|
151
|
-
dirs
|
|
147
|
+
Paths.absolute_xdg(env: env)
|
|
152
148
|
end
|
|
153
149
|
|
|
154
150
|
# The unsets that keep a `chi --model X` worker's default from passing
|
|
@@ -37,7 +37,7 @@ module Samagotchi
|
|
|
37
37
|
# @param relay [#open_question, #relay_desk] Peers#relay
|
|
38
38
|
# @return [Boolean] a relay can take this question
|
|
39
39
|
def relayable?(relay, question)
|
|
40
|
-
!relay.nil? && question.is_a?(
|
|
40
|
+
!relay.nil? && question.is_a?(PendingQuestion) && question.kind.to_s == Guardrails::Approval::KIND
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Relay the child's pending approval and wait until it settles.
|
|
@@ -45,7 +45,7 @@ module Samagotchi
|
|
|
45
45
|
# @return [Outcome]
|
|
46
46
|
def call(child_id, question, relay:, state_dir:, more: 0)
|
|
47
47
|
child = Session.load(child_id, state_dir: state_dir)
|
|
48
|
-
qid = question
|
|
48
|
+
qid = question.id.to_s
|
|
49
49
|
desk = relay.relay_desk
|
|
50
50
|
relay_id = desk.open(child_id: child_id, child_question_id: qid)
|
|
51
51
|
client = BridgeClient.discover(child_id, session_dir: Session.session_dir(child_id, state_dir: state_dir))
|
|
@@ -70,7 +70,7 @@ module Samagotchi
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
pending = Session.load(child_id, state_dir: state_dir).pending_question
|
|
73
|
-
pending && pending
|
|
73
|
+
pending && pending.id.to_s == qid ? nil : "answered_on_child"
|
|
74
74
|
rescue ArgumentError
|
|
75
75
|
"child_gone"
|
|
76
76
|
end
|
|
@@ -175,7 +175,7 @@ module Samagotchi
|
|
|
175
175
|
return if waiting.empty?
|
|
176
176
|
|
|
177
177
|
child_id, question = waiting.first
|
|
178
|
-
@done[question
|
|
178
|
+
@done[question.id.to_s] = true
|
|
179
179
|
outcome = DelegateRelay.call(child_id, question, relay: @relay, state_dir: @state_dir, more: waiting.size - 1)
|
|
180
180
|
@on_outcome.call(child_id, outcome)
|
|
181
181
|
# The next one, if any, right after.
|
|
@@ -186,7 +186,7 @@ module Samagotchi
|
|
|
186
186
|
# @return [Integer]
|
|
187
187
|
def count = waiting.size
|
|
188
188
|
|
|
189
|
-
# @return [Array<Array(String,
|
|
189
|
+
# @return [Array<Array(String, PendingQuestion)>] [child id, pending question],
|
|
190
190
|
# oldest question first
|
|
191
191
|
def waiting
|
|
192
192
|
rows = SessionManager.children_of(@parent_id, state_dir: @state_dir).select do |row|
|
|
@@ -194,10 +194,10 @@ module Samagotchi
|
|
|
194
194
|
end
|
|
195
195
|
rows.filter_map do |row|
|
|
196
196
|
pending = Session.load(row[:id], state_dir: @state_dir).pending_question
|
|
197
|
-
[row[:id], pending] if pending && pending
|
|
197
|
+
[row[:id], pending] if pending && pending.id.to_s == row[:waiting_id].to_s
|
|
198
198
|
rescue ArgumentError
|
|
199
199
|
nil
|
|
200
|
-
end.sort_by { |_, pending| pending
|
|
200
|
+
end.sort_by { |_, pending| pending.created_at.to_s }
|
|
201
201
|
end
|
|
202
202
|
|
|
203
203
|
private
|
|
@@ -91,7 +91,7 @@ module Samagotchi
|
|
|
91
91
|
# asked), the relayed question no longer reported: a turn that
|
|
92
92
|
# ends with no reply during the relay's POST or after it still
|
|
93
93
|
# ends the wait.
|
|
94
|
-
baseline = ReplyWait.baseline_of(wait.session, question_id: wait.question
|
|
94
|
+
baseline = ReplyWait.baseline_of(wait.session, question_id: wait.question.id)
|
|
95
95
|
end
|
|
96
96
|
rescue ArgumentError => e
|
|
97
97
|
"Error: #{e.message}"
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require "net/http"
|
|
4
4
|
require "uri"
|
|
5
5
|
require "ipaddr"
|
|
6
|
-
require "nokogiri"
|
|
7
6
|
require_relative "../version"
|
|
8
7
|
|
|
9
8
|
module Samagotchi
|
|
@@ -128,6 +127,9 @@ module Samagotchi
|
|
|
128
127
|
def self.extract_text_from_html(html)
|
|
129
128
|
return "Error: empty HTML response" if html.nil? || html.empty?
|
|
130
129
|
|
|
130
|
+
# Load lazily so workers don't pay for Nokogiri on every start.
|
|
131
|
+
require "nokogiri"
|
|
132
|
+
|
|
131
133
|
doc = Nokogiri::HTML(html)
|
|
132
134
|
|
|
133
135
|
# Extract noscript text before removing it
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -43,7 +43,7 @@ require_relative "../recap_store"
|
|
|
43
43
|
require_relative "markdown_renderer"
|
|
44
44
|
require_relative "capabilities"
|
|
45
45
|
require_relative "editor"
|
|
46
|
-
require_relative "message_parts"
|
|
46
|
+
require_relative "../message_parts"
|
|
47
47
|
require_relative "session_summary"
|
|
48
48
|
require_relative "../log"
|
|
49
49
|
require_relative "../paths"
|
|
@@ -855,7 +855,7 @@ module Samagotchi
|
|
|
855
855
|
current_turn = turn_snapshot["current_turn"]
|
|
856
856
|
current_turn ? current_turn["pending_question"] : turn_snapshot["pending_question"]
|
|
857
857
|
elsif owner&.worker?
|
|
858
|
-
session.pending_question
|
|
858
|
+
session.pending_question&.to_h
|
|
859
859
|
end
|
|
860
860
|
end
|
|
861
861
|
|
|
@@ -1794,6 +1794,9 @@ module Samagotchi
|
|
|
1794
1794
|
# before the next turn streams (live over saved).
|
|
1795
1795
|
context: source["context"],
|
|
1796
1796
|
tokens: source["tokens"],
|
|
1797
|
+
# The tokens, cost and speeds per model (SessionMetrics'
|
|
1798
|
+
# model_segments; none with one model, or from an older file).
|
|
1799
|
+
model_segments: Array(source["model_segments"]),
|
|
1797
1800
|
# The memory indexes the session's prompt holds (the ctx tooltip).
|
|
1798
1801
|
memory_index: source["memory_index"]
|
|
1799
1802
|
}
|
|
@@ -45,6 +45,7 @@ import { elementOf, emptyAnswerTemplate } from "./turn_html.js";
|
|
|
45
45
|
import { createBubbles, hintEl, outputBubbleTemplate, setBubbleDelivery } from "./bubbles.js";
|
|
46
46
|
import { createCardsPlace } from "./cards_place.js";
|
|
47
47
|
import { createStageView, FOLD_MS } from "./stage_view.js";
|
|
48
|
+
import { createMarkdownPlace } from "./markdown_place.js";
|
|
48
49
|
import { reducedMotion, STAGE_GAP } from "./stage_frame.js";
|
|
49
50
|
import { chainNeighbours, continueRefusal, continuedText, freshLink, openChildrenTemplate, openerText, openTitle } from "./chains.js";
|
|
50
51
|
import { createTaskStopper } from "./task_stop.js";
|
|
@@ -86,6 +87,7 @@ import { liveSlots } from "./stage_model.js";
|
|
|
86
87
|
import { currentGen } from "./turn_model.js";
|
|
87
88
|
import { toolName } from "./activity.js";
|
|
88
89
|
import { toastBottom } from "./toast_place.js";
|
|
90
|
+
import { createToasts } from "./toasts.js";
|
|
89
91
|
|
|
90
92
|
const $ = (s) => document.querySelector(s);
|
|
91
93
|
const topStripEl = $("#topStrip");
|
|
@@ -231,26 +233,25 @@ const viewArgs = {
|
|
|
231
233
|
// the route (its sidecar features).
|
|
232
234
|
taskStop: createTaskStopper({ sessionId: () => selected, features: () => current.workerFeatures }),
|
|
233
235
|
};
|
|
236
|
+
const markdownPlace = createMarkdownPlace({
|
|
237
|
+
capabilities: () => pageCapabilities,
|
|
238
|
+
renderText,
|
|
239
|
+
roots: () => turnRoots(),
|
|
240
|
+
touchedFiles,
|
|
241
|
+
currentDir: () => current.dir,
|
|
242
|
+
linkRefs,
|
|
243
|
+
applyFinalAnswer,
|
|
244
|
+
findInTurnRoots: (selector) => findInTurnRoots(selector),
|
|
245
|
+
normalize,
|
|
246
|
+
pairAnswerRefs,
|
|
247
|
+
});
|
|
248
|
+
const { renderMarkdown, linkRefsIn, applyFinalMarkdown, linkTextRefs } = markdownPlace;
|
|
234
249
|
const turnDom = view === "stage"
|
|
235
250
|
? createStageView({
|
|
236
251
|
...viewArgs, isNearBottom: () => atHistoryEnd(), dockEl: $("#dock"), centerEl: $("#center"), composerEl,
|
|
237
252
|
renderMarkdown, linkRefs: linkRefsIn,
|
|
238
253
|
})
|
|
239
254
|
: createTurnView(viewArgs);
|
|
240
|
-
// A text the page already shows as its markdown HTML (POST /api/render,
|
|
241
|
-
// sanitised there), or null: markdown off here (no request), or a failed
|
|
242
|
-
// read (the raw text stays).
|
|
243
|
-
function renderMarkdown(text) {
|
|
244
|
-
if (pageCapabilities.display !== true) return Promise.resolve(null);
|
|
245
|
-
return renderText(text)
|
|
246
|
-
.then((d) => (typeof d?.html === "string" && d.html ? d.html : null))
|
|
247
|
-
.catch(() => null);
|
|
248
|
-
}
|
|
249
|
-
// The `path:line` refs in one rendered text (the stage's asking slot, a
|
|
250
|
-
// kept reply), as linkTextRefs links the answers'.
|
|
251
|
-
function linkRefsIn(el) {
|
|
252
|
-
linkRefs(el, touchedFiles(turnRoots()), current.dir);
|
|
253
|
-
}
|
|
254
255
|
// Where a running turn's rows can be: the history, and the stage while it
|
|
255
256
|
// holds a turn (lookups by id, copy, annotate, card actions).
|
|
256
257
|
function turnRoots() {
|
|
@@ -978,32 +979,6 @@ async function readTurnEnd() {
|
|
|
978
979
|
} catch(_){}
|
|
979
980
|
}
|
|
980
981
|
|
|
981
|
-
// The turn's answer as its rendered markdown and with its plugin refs,
|
|
982
|
-
// once it is final (the turn-end read, an answer_display re-read:
|
|
983
|
-
// answer_refs.js applyFinalAnswer); then the refs.
|
|
984
|
-
function applyFinalMarkdown(messages) {
|
|
985
|
-
applyFinalAnswer(messages, findInTurnRoots(".bubble.output"), normalize);
|
|
986
|
-
linkTextRefs();
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// The refs on the answers' and closed steps' texts, on this page: first
|
|
990
|
-
// each answer's own (a plugin's, answer_refs.js), then the `path:line`
|
|
991
|
-
// refs that name a file the session's rows (and cards) touched
|
|
992
|
-
// (ref_scan.js), which leave a plugin's alone. Not mid-stream: at a
|
|
993
|
-
// turn's end and after a reload's render. A ref already made stays as it
|
|
994
|
-
// is.
|
|
995
|
-
function linkTextRefs() {
|
|
996
|
-
const roots = turnRoots();
|
|
997
|
-
const touched = touchedFiles(roots);
|
|
998
|
-
for (const root of roots) {
|
|
999
|
-
for (const el of root.querySelectorAll(".bubble.output")) pairAnswerRefs(el);
|
|
1000
|
-
}
|
|
1001
|
-
if (!touched.size) return;
|
|
1002
|
-
for (const root of roots) {
|
|
1003
|
-
for (const el of root.querySelectorAll(".bubble.output, .gen:not(.live) > .gen-text")) linkRefs(el, touched, current.dir);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
982
|
function seedSeenFromDom() {
|
|
1008
983
|
shownTexts.seed(findInTurnRoots(".bubble").map((b) => normalize(b.textContent)));
|
|
1009
984
|
}
|
|
@@ -1475,7 +1450,7 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1475
1450
|
// The chips stay (uploaded ones keep their ref, to sentTo) for another try: an
|
|
1476
1451
|
// image refused (bad_image, too_large, bad_images) says why below.
|
|
1477
1452
|
if (e.code === "owned_by_tui") {
|
|
1478
|
-
if (sentTo !== selected) return
|
|
1453
|
+
if (sentTo !== selected) return showToast(cantText(`send to ${sentTo.slice(0, 8)}`, e), null, { sticky: true });
|
|
1479
1454
|
current.owner = "tui";
|
|
1480
1455
|
updateComposerMode();
|
|
1481
1456
|
return;
|
|
@@ -1485,7 +1460,7 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1485
1460
|
if (e.code === "not_found") return leaveGoneSession(sentTo, "not_found");
|
|
1486
1461
|
// A /queue to a worker that can't queue (it would steer): its words, the text kept.
|
|
1487
1462
|
if (e.code === "delivery_unavailable" && sentTo === selected) return addCommandBubble({ label: null, line, text: e.body?.detail || e.message, failed: true });
|
|
1488
|
-
|
|
1463
|
+
showToast(cantText("send", e), null, { sticky: true });
|
|
1489
1464
|
} finally {
|
|
1490
1465
|
actionBtn.disabled = current.owner === "tui";
|
|
1491
1466
|
promptEl.focus();
|
|
@@ -1502,13 +1477,14 @@ const composerCommands = createComposerCommands({
|
|
|
1502
1477
|
refreshHistory: () => refreshHistory(),
|
|
1503
1478
|
failed: (line, text) => addCommandBubble({ label: null, line, text, failed: true }),
|
|
1504
1479
|
// As handleSendTurn says a turn's failure after a switch.
|
|
1505
|
-
failedElsewhere: (line, text) =>
|
|
1480
|
+
failedElsewhere: (line, text, id) => showToast(cantText(`run ${line} in ${id.slice(0, 8)}`, text), null, { sticky: true }),
|
|
1506
1481
|
ownedByTui: () => {
|
|
1507
1482
|
current.owner = "tui";
|
|
1508
1483
|
updateComposerMode();
|
|
1509
1484
|
},
|
|
1510
1485
|
composer: {
|
|
1511
1486
|
busy: () => { actionBtn.disabled = true; },
|
|
1487
|
+
text: () => promptEl.value,
|
|
1512
1488
|
clear: () => clearComposer(),
|
|
1513
1489
|
done: () => {
|
|
1514
1490
|
actionBtn.disabled = current.owner === "tui";
|
|
@@ -2052,24 +2028,11 @@ function placeToast() {
|
|
|
2052
2028
|
else root.removeProperty("--toast-bottom");
|
|
2053
2029
|
}
|
|
2054
2030
|
}
|
|
2031
|
+
const toasts = createToasts({ el: $("#toast"), onChange: placeToast });
|
|
2055
2032
|
// The model panel opening, closing or changing height (its list filters).
|
|
2056
2033
|
new ResizeObserver(() => placeToast()).observe(document.getElementById("modelPanel"));
|
|
2057
2034
|
|
|
2058
|
-
|
|
2059
|
-
// words, or a template (chains.js openChildrenTemplate: a link in it, a
|
|
2060
|
-
// delegate a continue waits for, opens that session and hides the toast).
|
|
2061
|
-
let toastTimer = null;
|
|
2062
|
-
function showToast(text, action = null, { sticky = false } = {}) {
|
|
2063
|
-
const el = $("#toast");
|
|
2064
|
-
const hide = () => { el.hidden = true; };
|
|
2065
|
-
render(html`<span @click=${(e) => { if (e.target.closest?.("a[href]")) hide(); }}>${text}</span>${action
|
|
2066
|
-
? html`<button type="button" class="toast-action" @click=${() => { hide(); action.run(); }}>${action.label}</button>`
|
|
2067
|
-
: nothing}${sticky ? html`<button type="button" class="toast-close" title="Dismiss" aria-label="Dismiss" @click=${hide}>✕</button>` : nothing}`, el);
|
|
2068
|
-
el.hidden = false;
|
|
2069
|
-
placeToast();
|
|
2070
|
-
clearTimeout(toastTimer);
|
|
2071
|
-
if (!sticky) toastTimer = setTimeout(hide, 6000);
|
|
2072
|
-
}
|
|
2035
|
+
function showToast(text, action = null, options = {}) { toasts.show(text, action, options); }
|
|
2073
2036
|
|
|
2074
2037
|
// Annotate (annotate_bar.js): a selection in the conversation offers
|
|
2075
2038
|
// "Annotate" and the presets, which append a quote to the composer, and the
|
|
@@ -230,7 +230,7 @@ export function chainRowsTemplate(f, { activeId = null, now = Date.now() } = {})
|
|
|
230
230
|
const ago = relativeTime(s.updated_at || s.created_at || "", now);
|
|
231
231
|
const tip = `${s.id} · ${s.status || ""}${ago ? ` · updated ${ago}` : ""}`;
|
|
232
232
|
return html`<div class=${cls.join(" ")} data-id=${s.id} style=${"--depth:1"} role=${"listitem"}><button type="button" class="fr-open"
|
|
233
|
-
title=${tip}>${rowDotTemplate(s, waiting)}<span class="cr-
|
|
233
|
+
title=${tip}>${rowDotTemplate(s, waiting)}<span class="cr-n">#${n}</span>${
|
|
234
234
|
date ? html`<span class=${date.length > 10 ? "cr-date timed" : "cr-date"}>${date}</span>` : nothing}<span class="fr-preview">${s.recap || chainTitle(s, byId)}</span>${
|
|
235
235
|
rowBadgesTemplate(waiting, stopped)}${s.archived ? archivedBadge : nothing}</button></div>`;
|
|
236
236
|
});
|