samagotchi 0.155.0 → 0.157.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 +57 -1
- data/bin/chi +2 -1
- data/docs/architecture.md +22 -13
- data/docs/bundles.md +3 -1
- data/docs/cli.md +9 -8
- data/docs/configuration.md +36 -4
- data/docs/guardrails.md +9 -5
- data/docs/hooks.md +11 -5
- data/docs/internals/anthropic.md +1 -1
- data/docs/internals/attached-context.md +3 -2
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +83 -6
- data/docs/internals/bundles.md +28 -18
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +641 -937
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +2 -2
- data/docs/internals/participants.md +3 -2
- data/docs/internals/prompt-caching.md +4 -1
- data/docs/internals/remote-sessions.md +8 -3
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +2 -2
- data/docs/memory.md +13 -2
- data/docs/plugins.md +35 -2
- data/docs/sessions.md +11 -11
- data/docs/sub-agent.md +17 -2
- data/docs/system-one.md +3 -2
- data/docs/testing.md +53 -2
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +10 -4
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +109 -9
- data/lib/samagotchi/bridge/turn_drawings.rb +265 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/check.rb +18 -0
- data/lib/samagotchi/bundle_command/try/sandbox.rb +2 -2
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +5 -2
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/child_move.rb +13 -57
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +9 -23
- data/lib/samagotchi/config.rb +20 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_sources.rb +5 -4
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/context_judge.rb +19 -11
- data/lib/samagotchi/engine/messages.rb +1 -1
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +29 -11
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine/side_requests.rb +21 -11
- data/lib/samagotchi/engine/toolkit.rb +10 -9
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +7 -7
- data/lib/samagotchi/engine/verify.rb +1 -1
- data/lib/samagotchi/engine.rb +43 -12
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +6 -3
- data/lib/samagotchi/guardrails/targets.rb +24 -9
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +58 -46
- data/lib/samagotchi/kernel_loop.rb +53 -124
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/chat_loop.rb +40 -21
- data/lib/samagotchi/llm/native_backend.rb +1 -1
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- data/lib/samagotchi/llm_context_notice.rb +1 -1
- data/lib/samagotchi/llm_context_strategy.rb +29 -67
- data/lib/samagotchi/llm_context_view.rb +9 -0
- data/lib/samagotchi/memory_bundle/asset_installer.rb +71 -76
- data/lib/samagotchi/memory_bundle/builder.rb +2 -1
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_copy.rb +2 -1
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +8 -1
- data/lib/samagotchi/memory_bundle/installer.rb +185 -169
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +15 -1
- data/lib/samagotchi/memory_bundle/remerge.rb +25 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -6
- data/lib/samagotchi/memory_bundle/status.rb +14 -20
- data/lib/samagotchi/memory_bundle/system_bundle.rb +24 -6
- data/lib/samagotchi/memory_bundle/trash.rb +23 -8
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +7 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +2 -3
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/api.rb +41 -6
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/loader.rb +33 -10
- data/lib/samagotchi/plugin/sessions.rb +3 -6
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +27 -0
- data/lib/samagotchi/reminder_carry.rb +1 -1
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +36 -37
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +370 -445
- data/lib/samagotchi/session_access/local.rb +12 -11
- data/lib/samagotchi/session_access/remote.rb +22 -6
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_field.rb +133 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +139 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +118 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +28 -1541
- data/lib/samagotchi/session_metrics.rb +8 -5
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui/join_replay.rb +86 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +5 -7
- data/lib/samagotchi/testing/install.rb +2 -1
- data/lib/samagotchi/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +10 -4
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +25 -5
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/delegate.rb +55 -45
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +5 -5
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/tools/registry.rb +28 -11
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +52 -51
- data/lib/samagotchi/tools/tool_surface.rb +22 -8
- data/lib/samagotchi/turn_context.rb +2 -1
- data/lib/samagotchi/turn_stages/context_judge.rb +38 -36
- data/lib/samagotchi/turn_stages/objectives.rb +56 -52
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +13 -17
- data/lib/samagotchi/turn_stages/verify_anchors.rb +9 -10
- data/lib/samagotchi/turn_stages/warmup.rb +14 -15
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +45 -28
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +3 -3
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +6 -6
- data/lib/samagotchi/web/session_control.rb +4 -4
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +67 -5
- data/lib/samagotchi/web/session_summary.rb +39 -28
- data/lib/samagotchi/worker.rb +83 -32
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_link.rb +3 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +32 -4
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../log"
|
|
4
|
+
require_relative "recording"
|
|
5
|
+
require_relative "runs"
|
|
6
|
+
|
|
7
|
+
module Samagotchi
|
|
8
|
+
module SystemOne
|
|
9
|
+
# What a turn stage needs to save its System 1 asks as runs: the
|
|
10
|
+
# Engine's Runs and the session's model as the Engine names it now.
|
|
11
|
+
# Engine#system_one_recorder builds one fresh at each stage call (never
|
|
12
|
+
# kept: an outgoing ask after a before_turn hook's model switch names
|
|
13
|
+
# the new model), on the caller's thread; the stage keeps it only for
|
|
14
|
+
# that call. NONE is what a stage gets when the Engine couldn't build
|
|
15
|
+
# one: no runs, every client as it was.
|
|
16
|
+
#
|
|
17
|
+
# +runs+ [Runs, nil] where the runs go (Runs#on? false: system_one.runs
|
|
18
|
+
# off); nil: NONE
|
|
19
|
+
# +session_model+ [String, nil] Engine#effective_model_name at the call
|
|
20
|
+
Recorder = Data.define(:runs, :session_model) do
|
|
21
|
+
# +client+ (nil for none) saving its asks as +judgment+'s runs. Anything
|
|
22
|
+
# going wrong here leaves the client as it was: a run is never worth a
|
|
23
|
+
# judgment. A scratch session's asks aren't saved; a participant's
|
|
24
|
+
# turn is never judged, so it never comes here.
|
|
25
|
+
# @param mode [String, nil] the use's mode, for its runs (Recording.wrap)
|
|
26
|
+
def wrap(client, judgment, session, mode: nil)
|
|
27
|
+
return client if client.nil? || runs.nil? || session&.scratch
|
|
28
|
+
|
|
29
|
+
context = Recording::Context.of(session, session_model: session && session_model)
|
|
30
|
+
Recording.wrap(client, judgment: judgment, runs: runs, context: context, recorded_mode: mode)
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
Recorder.failed(e)
|
|
33
|
+
client
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Whether runs are saved (an ask whose only use is its run is made
|
|
37
|
+
# only then: ObjectivesAudit).
|
|
38
|
+
def saving? = !runs.nil? && runs.on?
|
|
39
|
+
|
|
40
|
+
# The one warn for a recording that can't be set up.
|
|
41
|
+
def self.failed(error)
|
|
42
|
+
Samagotchi::Log.warn(:system_one, "system_one_recording_failed", error: error.class.name, msg: error.message.to_s[0, 200])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# No recording: the plain client, no runs.
|
|
47
|
+
Recorder::NONE = Recorder.new(runs: nil, session_model: nil)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -613,39 +613,19 @@ module Samagotchi
|
|
|
613
613
|
lane = :writing
|
|
614
614
|
when :generation_chunk
|
|
615
615
|
tail, lane = event[:thinking].to_s.empty? ? [event[:text], :writing] : [event[:thinking], :thinking]
|
|
616
|
-
|
|
617
|
-
running_tool = event[:tool]
|
|
618
|
-
@renderer.call(event)
|
|
619
|
-
when :tool_call_completed
|
|
620
|
-
running_tool = nil
|
|
621
|
-
replay_tool_completed(event)
|
|
622
|
-
when :merged_input then @screen.commit("input> #{event[:content]}")
|
|
623
|
-
when :reminder_injected then @screen.commit(reminder_line(event[:reminders]))
|
|
624
|
-
# The turn's start (its images), a plugin's steer, and its rows (a
|
|
625
|
-
# hook's notice, a retry's line), as they were drawn live.
|
|
626
|
-
# Questions are left out: the pending one is asked after the
|
|
627
|
-
# replay, answered ones showed no line.
|
|
616
|
+
# The turn's start (its images), as it was drawn live.
|
|
628
617
|
when :turn_started then @renderer.call(without_named_images(event, turn[:prompt]))
|
|
629
|
-
|
|
630
|
-
|
|
618
|
+
# Its rows, as a drawn turn's (JoinReplay#replay_row).
|
|
619
|
+
else
|
|
620
|
+
running_tool = event[:tool] if event[:type] == :tool_call_started
|
|
621
|
+
running_tool = nil if event[:type] == :tool_call_completed
|
|
622
|
+
@join_replay.replay_row(event)
|
|
631
623
|
end
|
|
632
624
|
end
|
|
633
625
|
@view.resume(tail: tail, lane: lane, tool: running_tool, parts: turn[:parts])
|
|
634
626
|
ask(turn[:pending_question]) if turn[:pending_question]
|
|
635
627
|
end
|
|
636
628
|
|
|
637
|
-
# A finished tool call of the joined turn: the live row (action,
|
|
638
|
-
# duration). A snapshot from an older worker has no action: its row
|
|
639
|
-
# names the tool only.
|
|
640
|
-
def replay_tool_completed(event)
|
|
641
|
-
return @renderer.call({ duration_ms: nil }.merge(event)) if event.dig(:activity, :action)
|
|
642
|
-
|
|
643
|
-
activity = EventRenderer.described(event) || {}
|
|
644
|
-
@screen.commit(snapshot_tool_line({ tool: event[:tool], params: activity[:params], status: activity[:status],
|
|
645
|
-
description: activity[:description], output_truncated: event[:output_truncated],
|
|
646
|
-
images: event[:images], diff: event[:diff] }))
|
|
647
|
-
end
|
|
648
|
-
|
|
649
629
|
def start_turn(event)
|
|
650
630
|
@running = true
|
|
651
631
|
@joined_mid_turn = false
|
|
@@ -14,7 +14,7 @@ module Samagotchi
|
|
|
14
14
|
LATE = "worker not answering in time"
|
|
15
15
|
# An exit the worker agreed to in an empty session. Said before it has
|
|
16
16
|
# left: it checks again once it lets go, and a note or prompt arriving
|
|
17
|
-
# first keeps the session (
|
|
17
|
+
# first keeps the session (WorkerBoot.discard_left_session).
|
|
18
18
|
EMPTY_WORDS = "the session is empty: it will be discarded if nothing arrives before its worker stops."
|
|
19
19
|
EMPTY_EXIT = "Detached; #{EMPTY_WORDS}"
|
|
20
20
|
# Why the worker stayed up after /exit, by the reason it gave.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../bridge/turn_accumulator"
|
|
3
4
|
require_relative "../context_note"
|
|
4
5
|
require_relative "../image_store"
|
|
5
6
|
require_relative "../log"
|
|
@@ -31,11 +32,32 @@ module Samagotchi
|
|
|
31
32
|
@renderer = renderer
|
|
32
33
|
end
|
|
33
34
|
|
|
35
|
+
# A snapshot's last_turn_drawing (Bridge#snapshot: the last exchange's
|
|
36
|
+
# turn as live drew it) draws that turn's rows; without one (an older
|
|
37
|
+
# worker, a turn before drawings) they are rebuilt from the messages.
|
|
34
38
|
def render_join(snapshot, saved)
|
|
35
|
-
render_join_header(Array(snapshot[:messages]))
|
|
39
|
+
render_join_header(Array(snapshot[:messages]), drawing: snapshot[:last_turn_drawing])
|
|
36
40
|
render_join_gap(snapshot, saved)
|
|
37
41
|
end
|
|
38
42
|
|
|
43
|
+
# One replayed event of a turn's rows (TurnAccumulator.replay_events),
|
|
44
|
+
# as live drew it: a joined running turn's (AttachedLoop#render_current_turn)
|
|
45
|
+
# and a drawn finished one's (#render_drawn_turn). Its tool rows with
|
|
46
|
+
# their action and duration, merged input, reminders, a plugin's steer,
|
|
47
|
+
# a hook's notice, a retry's, a steer cut's and a ✂ row's line. The
|
|
48
|
+
# step text, the turn's start and questions are the caller's (the
|
|
49
|
+
# pending one is asked after the replay, answered ones showed no line).
|
|
50
|
+
def replay_row(event)
|
|
51
|
+
case event[:type]
|
|
52
|
+
when :tool_call_started then @renderer.call(event)
|
|
53
|
+
when :tool_call_completed then replay_tool_completed(event)
|
|
54
|
+
when :merged_input then @screen.commit("input> #{event[:content]}")
|
|
55
|
+
when :reminder_injected then @screen.commit(reminder_line(event[:reminders]))
|
|
56
|
+
when :pending_input_merged, :hook_notice, :empty_answer_retry, :steer_cut, :llm_context_edited
|
|
57
|
+
@renderer.call(event)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
39
61
|
# The snapshot's cards and between-turns notices (Bridge CardStore):
|
|
40
62
|
# on join the ones since the last turn (and the running turn's), as
|
|
41
63
|
# the join header shows the last exchange; on a resync only cards not
|
|
@@ -60,6 +82,18 @@ module Samagotchi
|
|
|
60
82
|
|
|
61
83
|
private
|
|
62
84
|
|
|
85
|
+
# A finished tool call: the live row (action, duration: none when the
|
|
86
|
+
# part has none, never the replay's own clock). A snapshot from an
|
|
87
|
+
# older worker has no action: its row names the tool only.
|
|
88
|
+
def replay_tool_completed(event)
|
|
89
|
+
return @renderer.call({ duration_ms: nil }.merge(event)) if event.dig(:activity, :action)
|
|
90
|
+
|
|
91
|
+
activity = EventRenderer.described(event) || {}
|
|
92
|
+
@screen.commit(snapshot_tool_line({ tool: event[:tool], params: activity[:params], status: activity[:status],
|
|
93
|
+
description: activity[:description], output_truncated: event[:output_truncated],
|
|
94
|
+
images: event[:images], diff: event[:diff] }))
|
|
95
|
+
end
|
|
96
|
+
|
|
63
97
|
# Where the session's last activity was: its newest stamped entry,
|
|
64
98
|
# else its saved recap's time. A gap ago (and no turn running): one
|
|
65
99
|
# line after the join block says when.
|
|
@@ -71,7 +105,7 @@ module Samagotchi
|
|
|
71
105
|
@screen.commit(gap_divider(line)) if line
|
|
72
106
|
end
|
|
73
107
|
|
|
74
|
-
def render_join_header(messages)
|
|
108
|
+
def render_join_header(messages, drawing: nil)
|
|
75
109
|
exchange = messages.select { |m| %w[user model assistant].include?(m[:role].to_s) }
|
|
76
110
|
# The id stays findable: the detach line and chi sessions list show it.
|
|
77
111
|
Log.session_id = @link.client.session_id
|
|
@@ -89,6 +123,11 @@ module Samagotchi
|
|
|
89
123
|
unless ImageRef.placeholder_only?(exchange[last_user][:content])
|
|
90
124
|
Array(exchange[last_user][:images]).each { |ref| @screen.commit(format_image_line(ref)) }
|
|
91
125
|
end
|
|
126
|
+
if (drawn = drawn_turn(drawing, exchange[last_user]))
|
|
127
|
+
render_drawn_turn(*drawn, exchange[(last_user + 1)..])
|
|
128
|
+
return render_join_notes(messages, from, after_exchange: true)
|
|
129
|
+
end
|
|
130
|
+
|
|
92
131
|
render_join_steps(messages, from, durations: join_tool_records(exchange[last_user]))
|
|
93
132
|
record = @link.saved_turn_record(exchange[last_user][:turn_id] || exchange[last_user]["turn_id"])
|
|
94
133
|
failed = join_failed_record(record)
|
|
@@ -109,16 +148,55 @@ module Samagotchi
|
|
|
109
148
|
@screen.commit(@view.turn_canceled_line(record["cancellation_reason"], record["duration_ms"], by: record["cancelled_by"]))
|
|
110
149
|
@screen.commit(@view.turn_end_hint(ROLLBACK_HINT)) unless record["continue"]
|
|
111
150
|
else
|
|
112
|
-
|
|
113
|
-
# be only a tool call.
|
|
114
|
-
answer = exchange[(last_user + 1)..].reverse_each
|
|
115
|
-
.map { |m| OutputFormatter.strip_markup(m[:content]) }
|
|
116
|
-
.find { |text| !text.empty? }
|
|
117
|
-
@screen.commit(last_lines(answer)) if answer
|
|
151
|
+
render_join_answer(exchange[(last_user + 1)..])
|
|
118
152
|
end
|
|
119
153
|
render_join_notes(messages, from, after_exchange: true)
|
|
120
154
|
end
|
|
121
155
|
|
|
156
|
+
# The last answer of the messages after the prompt (+after+). The saved
|
|
157
|
+
# answer is raw: the latest keeps its thinking, and one may be only a
|
|
158
|
+
# tool call.
|
|
159
|
+
def render_join_answer(after)
|
|
160
|
+
answer = after.reverse_each.map { |m| OutputFormatter.strip_markup(m[:content]) }.find { |text| !text.empty? }
|
|
161
|
+
@screen.commit(last_lines(answer)) if answer
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# The snapshot's drawing as [turn, end event] when it is +prompt+'s
|
|
165
|
+
# turn (a Continue's or a reminder's drawing isn't the prompt's: the
|
|
166
|
+
# rows are rebuilt), else nil.
|
|
167
|
+
def drawn_turn(drawing, prompt)
|
|
168
|
+
return nil unless drawing.is_a?(Hash) && drawing[:drawing].is_a?(Hash) && drawing[:end].is_a?(Hash)
|
|
169
|
+
|
|
170
|
+
turn = Bridge::TurnAccumulator.from_wire(drawing[:drawing])
|
|
171
|
+
id = prompt[:turn_id] || prompt["turn_id"]
|
|
172
|
+
return nil unless id && turn[:turn_id].to_s == id.to_s
|
|
173
|
+
|
|
174
|
+
[turn, EventRenderer.symbolize(Bridge::TurnAccumulator.from_wire(drawing[:end]))]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# The last turn from its drawing, through the renderer as live drew it
|
|
178
|
+
# (the running turn's join path, AttachedLoop#render_current_turn): its
|
|
179
|
+
# rows with their status and duration, a retry's line, a hook's notice,
|
|
180
|
+
# steers, merged input and reminders; no step text (the live ticker's)
|
|
181
|
+
# and no cards (the snapshot's cards are drawn after). Then its end: a
|
|
182
|
+
# cancel's or a failure's line from the end event, else the empty
|
|
183
|
+
# answer's notice or the saved answer (+after+, the messages after the
|
|
184
|
+
# prompt).
|
|
185
|
+
def render_drawn_turn(turn, ending, after)
|
|
186
|
+
Bridge::TurnAccumulator.replay_events(turn).each do |event|
|
|
187
|
+
# The prompt line named its images already.
|
|
188
|
+
event[:type] == :turn_started ? @renderer.call(event.except(:images)) : replay_row(event)
|
|
189
|
+
end
|
|
190
|
+
case ending[:type]
|
|
191
|
+
when :turn_canceled, :turn_failed then @renderer.call(ending)
|
|
192
|
+
else
|
|
193
|
+
empty = ending.dig(:turn_summary, :empty_answer)
|
|
194
|
+
empty ? @screen.commit(format_empty_answer_line(empty[:retries], empty[:refusal])) : render_join_answer(after)
|
|
195
|
+
end
|
|
196
|
+
# A call the turn never answered left the tool slot running.
|
|
197
|
+
@view.finish_thinking_spinner
|
|
198
|
+
end
|
|
199
|
+
|
|
122
200
|
# What the last exchange did, as the live turn drew it: each saved
|
|
123
201
|
# call's tool row (action, params, status from its result, and its
|
|
124
202
|
# duration from +durations+, the turn's saved tool records) and a
|
|
@@ -112,13 +112,13 @@ module Samagotchi
|
|
|
112
112
|
# no turn id (an older session) or a session with no analytics.
|
|
113
113
|
def saved_tool_records(turn_id)
|
|
114
114
|
SessionMetrics.saved_tool_records(session_dir, turn_id)
|
|
115
|
-
rescue
|
|
115
|
+
rescue SessionPaths::InvalidId
|
|
116
116
|
[]
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
def saved_turn_record(turn_id)
|
|
120
120
|
SessionMetrics.saved_turn_record(session_dir, turn_id)
|
|
121
|
-
rescue
|
|
121
|
+
rescue SessionPaths::InvalidId
|
|
122
122
|
nil
|
|
123
123
|
end
|
|
124
124
|
|
|
@@ -44,8 +44,6 @@ module Samagotchi
|
|
|
44
44
|
# Raised when another process (a worker or another chi) owns the session
|
|
45
45
|
# this run was asked to use.
|
|
46
46
|
class SessionBusy < StandardError; end
|
|
47
|
-
# --resume with an id that has no saved session.
|
|
48
|
-
class SessionNotFound < StandardError; end
|
|
49
47
|
|
|
50
48
|
EMPTY_ANSWER_ERROR = "chi: the model gave an empty answer"
|
|
51
49
|
|
|
@@ -92,7 +90,9 @@ module Samagotchi
|
|
|
92
90
|
# As typed: the Engine applies an alias where it resolves the model.
|
|
93
91
|
flag_model_name = model_name.to_s.strip.empty? ? nil : ModelProfile.required_model_name(model_name)
|
|
94
92
|
@effective_model_name = flag_model_name || @default_model_name
|
|
95
|
-
|
|
93
|
+
# Taken once (as the Engine's); a client (specs) goes on a registry of
|
|
94
|
+
# its own, never the shared one.
|
|
95
|
+
@host_registry = host_registry || (client ? Samagotchi::HostRegistry.new : Samagotchi::HostRegistry.current)
|
|
96
96
|
# An injected client (specs) stands in for every host's client.
|
|
97
97
|
@host_registry.client_override = client if client
|
|
98
98
|
@client = @host_registry.resolve(@effective_model_name).client
|
|
@@ -100,9 +100,7 @@ module Samagotchi
|
|
|
100
100
|
# between the load and the lock that this run would later save over.
|
|
101
101
|
# An unknown id is refused before the claim, which would leave an
|
|
102
102
|
# empty session dir behind.
|
|
103
|
-
if session_id && !Session.exist?(session_id)
|
|
104
|
-
raise SessionNotFound, "Session not found: #{session_id}"
|
|
105
|
-
end
|
|
103
|
+
raise Session::NotFound, session_id if session_id && !Session.exist?(session_id)
|
|
106
104
|
|
|
107
105
|
claim_session!(session_id) if session_id
|
|
108
106
|
@resume_session = session_id ? Session.load(session_id) : nil
|
|
@@ -434,7 +432,7 @@ module Samagotchi
|
|
|
434
432
|
|
|
435
433
|
# A fresh session reaches the disk before its first turn: a tool in that
|
|
436
434
|
# turn reads it from there (delegate loads its parent to start a child,
|
|
437
|
-
# "
|
|
435
|
+
# "no session …" otherwise).
|
|
438
436
|
def save_before_first_turn(session)
|
|
439
437
|
session.save unless Session.exist?(session.id)
|
|
440
438
|
end
|
|
@@ -23,7 +23,8 @@ module Samagotchi
|
|
|
23
23
|
module Install
|
|
24
24
|
# warnings: the Installer's; plugin_failures / hook_failures:
|
|
25
25
|
# [Guardrails::LoadFailures::Failure] of this bundle, each location
|
|
26
|
-
# in the source dir
|
|
26
|
+
# in the source dir (hook_failures: the guardrails', a required
|
|
27
|
+
# plugin's among them); registered: what it registered (#registered).
|
|
27
28
|
Result = Data.define(:name, :warnings, :plugin_failures, :hook_failures, :registered) do
|
|
28
29
|
def failures = plugin_failures + hook_failures
|
|
29
30
|
end
|
data/lib/samagotchi/thinking.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "config"
|
|
4
|
-
require_relative "model_profile"
|
|
5
4
|
require_relative "setting_explained"
|
|
6
5
|
|
|
7
6
|
module Samagotchi
|
|
@@ -79,30 +78,23 @@ module Samagotchi
|
|
|
79
78
|
nil
|
|
80
79
|
end
|
|
81
80
|
|
|
82
|
-
# The level for +
|
|
81
|
+
# The level for +lookup+'s model and where it came from ("session",
|
|
83
82
|
# "--thinking", "models: qwen", "hosts.work", "thinking.level"; nil for
|
|
84
83
|
# the default).
|
|
85
|
-
# @param
|
|
86
|
-
# @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
|
|
87
|
-
# @param models [Hash{String => ModelSettings}, nil] ConfigFile.model_settings (specs)
|
|
84
|
+
# @param lookup [ModelLookup] the model's layers (EffectiveModel#lookup)
|
|
88
85
|
# @param session [Symbol, nil] the session's own level (Session#thinking)
|
|
89
86
|
# @return [Array(Symbol, String|nil)]
|
|
90
|
-
def resolve(
|
|
87
|
+
def resolve(lookup, session: nil)
|
|
91
88
|
return [session, SESSION_SOURCE] if session
|
|
92
89
|
|
|
93
|
-
global, origin = global_level
|
|
90
|
+
global, origin = global_level(lookup)
|
|
94
91
|
return [global, origin == :cli ? "--thinking" : "SAMAGOTCHI_THINKING_LEVEL"] if global && %i[cli env].include?(origin)
|
|
95
92
|
|
|
96
|
-
|
|
97
|
-
ConfigFile.model_settings
|
|
98
|
-
rescue StandardError
|
|
99
|
-
{}
|
|
100
|
-
end
|
|
101
|
-
setting = ConfigFile.model_setting(names, :thinking, models: models)
|
|
93
|
+
setting = lookup.model(:thinking)
|
|
102
94
|
return [setting.value, "models: #{setting.key}"] if setting
|
|
103
95
|
|
|
104
|
-
host =
|
|
105
|
-
return [host, "hosts.#{
|
|
96
|
+
host = lookup.host(:thinking)
|
|
97
|
+
return [host, "hosts.#{lookup.host_name}"] if host
|
|
106
98
|
return [global, "thinking.level"] if global
|
|
107
99
|
|
|
108
100
|
[DEFAULT, nil]
|
|
@@ -110,14 +102,15 @@ module Samagotchi
|
|
|
110
102
|
|
|
111
103
|
# #resolve, as a SettingExplained with the session's own level beside it.
|
|
112
104
|
# @return [SettingExplained]
|
|
113
|
-
def explain(
|
|
114
|
-
level, source = resolve(
|
|
105
|
+
def explain(lookup, session: nil)
|
|
106
|
+
level, source = resolve(lookup, session: session)
|
|
115
107
|
SettingExplained.new(level: level, source: source, own: session)
|
|
116
108
|
end
|
|
117
109
|
|
|
118
110
|
# thinking.level as a level and its origin (:cli, :env, :file), or nil.
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
# @param lookup [ModelLookup] whose env the key is read with
|
|
112
|
+
def global_level(lookup)
|
|
113
|
+
raw, origin = lookup.global("thinking.level")
|
|
121
114
|
return nil if origin == :default
|
|
122
115
|
|
|
123
116
|
where = { cli: "--thinking", env: "SAMAGOTCHI_THINKING_LEVEL" }.fetch(origin, "thinking.level")
|
|
@@ -139,13 +132,15 @@ module Samagotchi
|
|
|
139
132
|
# only the tail changes (llama.cpp and Splash keep the rest).
|
|
140
133
|
# @param target [HostRegistry::ModelTarget]
|
|
141
134
|
# @param profile [String, nil] the native prompt profile's name
|
|
135
|
+
# @param inferred [String, nil] the profile a local chat host's model
|
|
136
|
+
# name says (ModelProfile.inferred_profile_name of its bare id)
|
|
142
137
|
# @return [Symbol]
|
|
143
|
-
def cache_cost(target, profile: nil)
|
|
138
|
+
def cache_cost(target, profile: nil, inferred: nil)
|
|
144
139
|
entry = target.entry
|
|
145
140
|
if entry&.chat?
|
|
146
141
|
return :provider if entry.remote?
|
|
147
142
|
|
|
148
|
-
return
|
|
143
|
+
return inferred == "gemma4" ? :full : :tail
|
|
149
144
|
end
|
|
150
145
|
|
|
151
146
|
profile == "gemma4" ? :full : :tail
|
|
@@ -23,7 +23,7 @@ module Samagotchi
|
|
|
23
23
|
class TokenUsage < Data.define(:prompt_tokens, :completion_tokens, :cached_tokens, :cache_write_tokens,
|
|
24
24
|
:reasoning_tokens, :cost, :predicted_per_second, :prompt_per_second, :predicted_ms, :source)
|
|
25
25
|
CHARS_PER_TOKEN = 4.0
|
|
26
|
-
# Re-prefilled tokens under this are none (
|
|
26
|
+
# Re-prefilled tokens under this are none (ReprefillMeter#tokens,
|
|
27
27
|
# SessionMetrics): servers count the cache in blocks (OpenAI's 128).
|
|
28
28
|
REPREFILL_MIN_TOKENS = 128
|
|
29
29
|
|
|
@@ -5,6 +5,7 @@ require_relative "log"
|
|
|
5
5
|
require_relative "command_steps"
|
|
6
6
|
require_relative "tool_view"
|
|
7
7
|
require_relative "tools/execute"
|
|
8
|
+
require_relative "tools/registry"
|
|
8
9
|
require_relative "tools/read"
|
|
9
10
|
require_relative "tools/write"
|
|
10
11
|
require_relative "tools/memory"
|
|
@@ -359,8 +360,9 @@ module Samagotchi
|
|
|
359
360
|
name == "git" && args.first == "diff" && args.intersect?(%w[--quiet --exit-code])
|
|
360
361
|
end
|
|
361
362
|
|
|
362
|
-
# +surface+: the turn's Tools::ToolSurface,
|
|
363
|
-
# (call_tool's); nil outside a
|
|
363
|
+
# +surface+: the turn's Tools::ToolSurface, which a tool's entry is
|
|
364
|
+
# looked up in, for a preview that reads it (call_tool's); nil outside a
|
|
365
|
+
# turn (+registry+'s entry then).
|
|
364
366
|
def tool_activity_params(tool_name, call, registry: nil, surface: nil)
|
|
365
367
|
case tool_name
|
|
366
368
|
when Tools::Execute::NAME
|
|
@@ -455,7 +457,8 @@ module Samagotchi
|
|
|
455
457
|
parts << "options=#{preview_tool_param(Array(opts).join(","))}" if opts && !Array(opts).empty?
|
|
456
458
|
parts.join(" ")
|
|
457
459
|
else
|
|
458
|
-
registry_params(registry_entry(registry, tool_name), call,
|
|
460
|
+
registry_params(surface ? surface.registered[tool_name.to_s] : registry_entry(registry, tool_name), call,
|
|
461
|
+
surface)
|
|
459
462
|
end
|
|
460
463
|
end
|
|
461
464
|
|
|
@@ -473,7 +476,8 @@ module Samagotchi
|
|
|
473
476
|
# key="value" (the parsers' args:, which a registry-less reader such as
|
|
474
477
|
# the web's reload also has); nil for a tool the registry doesn't know
|
|
475
478
|
# that has no args:, and for a built-in without its own words above
|
|
476
|
-
# (the reminder tools).
|
|
479
|
+
# (the reminder tools). A missing surface (Tools::MissingSurface) is
|
|
480
|
+
# raised, never taken as a failing preview.
|
|
477
481
|
def registry_params(entry, call, surface = nil)
|
|
478
482
|
return nil if entry&.core?
|
|
479
483
|
return nil if entry.nil? && !call[:args].is_a?(Hash)
|
|
@@ -481,6 +485,8 @@ module Samagotchi
|
|
|
481
485
|
if entry&.preview
|
|
482
486
|
begin
|
|
483
487
|
return entry.preview_of(call, surface)
|
|
488
|
+
rescue Tools::MissingSurface
|
|
489
|
+
raise
|
|
484
490
|
rescue StandardError => e
|
|
485
491
|
Log.warn(:plugins, "plugin_preview_failed", tool: entry.name, error: e.class.name,
|
|
486
492
|
msg: "#{entry.name} preview failed: #{e.message}")
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "tools/args"
|
|
5
|
-
require_relative "
|
|
5
|
+
require_relative "config"
|
|
6
6
|
require_relative "tools/memory"
|
|
7
7
|
|
|
8
8
|
module Samagotchi
|
|
@@ -482,7 +482,7 @@ module Samagotchi
|
|
|
482
482
|
# (Tools::Registry's layer gate), so it is not in TOOL_SCHEMAS, which
|
|
483
483
|
# every turn declares. Its description carries llm_context.policy
|
|
484
484
|
# (#forget_outputs_schema); this one the default's.
|
|
485
|
-
FORGET_OUTPUTS_POLICY =
|
|
485
|
+
FORGET_OUTPUTS_POLICY = Config::DEFAULT_LLM_CONTEXT_POLICY
|
|
486
486
|
FORGET_OUTPUTS_DESCRIPTION = "Forget tool outputs you no longer need, to free context. Every tool output starts " \
|
|
487
487
|
"with an id like [#t42]: list ids to forget, and each output is replaced in place by " \
|
|
488
488
|
"a stub with your note (the call stays). %<policy>s\n" \
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "config"
|
|
3
4
|
require_relative "tool_output_files"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
@@ -28,6 +29,22 @@ module Samagotchi
|
|
|
28
29
|
# task's log, the child's reply in its output/ folder), which the tool
|
|
29
30
|
# gave with its text (Tools::RefText) and the gap line names.
|
|
30
31
|
module ToolOutputCut
|
|
32
|
+
# The per-output cap with nothing set (max_tool_output_chars).
|
|
33
|
+
DEFAULT_MAX_CHARS = 10_000
|
|
34
|
+
|
|
35
|
+
# The per-output cap both loops cut tool outputs to (and the emitted
|
|
36
|
+
# tool call events carry): an explicit +override+ wins, then
|
|
37
|
+
# max_tool_output_chars (Config). A non-positive value falls back to
|
|
38
|
+
# DEFAULT_MAX_CHARS (there is intentionally no "unlimited": live UIs get
|
|
39
|
+
# a bounded `output:` plus a truthful `output_truncated:` flag). The one
|
|
40
|
+
# exception is memory_read, which ToolRunner leaves out of this cap
|
|
41
|
+
# (memory.read_max_chars, 0: no fixed cap; a memory is read whole).
|
|
42
|
+
# @return [Integer]
|
|
43
|
+
def self.max_chars(override)
|
|
44
|
+
parsed = (override || Config.get("max_tool_output_chars")).to_i
|
|
45
|
+
parsed.positive? ? parsed : DEFAULT_MAX_CHARS
|
|
46
|
+
end
|
|
47
|
+
|
|
31
48
|
POLICIES = { "execute" => :head_tail_file, "task_wait" => :head_tail_ref, "delegate" => :head_tail_ref,
|
|
32
49
|
"delegate_result" => :head_tail_ref }.freeze
|
|
33
50
|
|
|
@@ -35,15 +35,17 @@ module Samagotchi
|
|
|
35
35
|
# #interrupted run for every call left (the one it cut short too), so
|
|
36
36
|
# the loop's conversation answers each call, then goes on up: the turn
|
|
37
37
|
# keeps its progress (Engine#ctrl_c_messages). +context+: the calls'
|
|
38
|
-
# turn (TurnContext), each run's (ToolRunner#run).
|
|
38
|
+
# turn (TurnContext), each run's (ToolRunner#run). +ids+: the calls'
|
|
39
|
+
# ToolIds, in call order (ToolIds.next_ids before the batch: the ids the
|
|
40
|
+
# entries are saved under), each on its run's tool events; nil: none.
|
|
39
41
|
# @return [Array<ToolRun>] the runs
|
|
40
|
-
def run_batch(runner, calls, context:, iteration:, emit:, on_stream_event:, cap:)
|
|
42
|
+
def run_batch(runner, calls, context:, iteration:, emit:, on_stream_event:, cap:, ids: nil)
|
|
41
43
|
# Before anything can be interrupted: the rescue counts it.
|
|
42
44
|
runs = []
|
|
43
45
|
emit.call({ type: :tool_dispatch_started, iteration: iteration, call_count: calls.length })
|
|
44
46
|
calls.each_with_index do |call, index|
|
|
45
47
|
runs << runner.run(call, context: context, iteration: iteration, call_index: index + 1, call_count: calls.length,
|
|
46
|
-
on_stream_event: on_stream_event, max_tool_output_chars: cap)
|
|
48
|
+
on_stream_event: on_stream_event, max_tool_output_chars: cap, tool_id: ids&.[](index))
|
|
47
49
|
yield runs.last, index if block_given?
|
|
48
50
|
end
|
|
49
51
|
emit.call({ type: :tool_dispatch_completed, iteration: iteration, call_count: calls.length })
|
|
@@ -86,7 +86,10 @@ module Samagotchi
|
|
|
86
86
|
# file (EditPreview.change; also on tool_call_completed, never in the
|
|
87
87
|
# model's output)
|
|
88
88
|
# @param context [TurnContext] the call's turn
|
|
89
|
-
|
|
89
|
+
# @param tool_id [String, nil] the id the run's output is saved under
|
|
90
|
+
# (ToolIds, handed out before the batch runs), on both tool events
|
|
91
|
+
# so a UI's row and the saved output share it
|
|
92
|
+
def run(call, context:, iteration:, call_index:, call_count:, on_stream_event:, max_tool_output_chars:, tool_id: nil)
|
|
90
93
|
# A call_tool of one of chi's own tools runs as that tool, from here
|
|
91
94
|
# on (the gate, the row, the result): rules, approvals and hooks see
|
|
92
95
|
# the real call, with via: (Tools::ToolIndex#unwrap).
|
|
@@ -114,6 +117,7 @@ module Samagotchi
|
|
|
114
117
|
started = { type: :tool_call_started, iteration: iteration, call_count: call_count, call_index: call_index,
|
|
115
118
|
tool: call[:name], call: call.dup, params: params }
|
|
116
119
|
started[:label] = label if label
|
|
120
|
+
started[:tool_id] = tool_id if tool_id
|
|
117
121
|
# Its row fields (ToolRowFields: title, called_as, via, view, ref, and a
|
|
118
122
|
# plugin tool's refs); the worker runs in its session's working
|
|
119
123
|
# directory.
|
|
@@ -121,6 +125,8 @@ module Samagotchi
|
|
|
121
125
|
refs = RowRefs.for(tools && !call[:name].nil? ? tools[call[:name]]&.refs : nil, call, cwd: session_cwd(context))
|
|
122
126
|
fields[:refs] = refs unless refs.empty?
|
|
123
127
|
started.merge!(fields)
|
|
128
|
+
# The row's clock: tool_call_completed's duration_ms runs from here.
|
|
129
|
+
started_clock = @clock.call
|
|
124
130
|
emit(on_stream_event, started)
|
|
125
131
|
|
|
126
132
|
# The ask comes after tool_call_started: the UI shows the tool line,
|
|
@@ -175,9 +181,20 @@ module Samagotchi
|
|
|
175
181
|
completed[:diff] = diff if diff
|
|
176
182
|
completed[:waited_ms] = waited_ms if waited_ms
|
|
177
183
|
completed[:output_file] = output_file if output_file
|
|
184
|
+
completed[:tool_id] = tool_id if tool_id
|
|
185
|
+
# A long output's web row (its hover): the cut a reload draws from the
|
|
186
|
+
# saved output (MessageParts#tool_part), so live and reload show the
|
|
187
|
+
# same text. The TUI keeps +output+.
|
|
188
|
+
if capped.length > ToolOutputCut::WEB_PREVIEW_CHARS
|
|
189
|
+
completed[:preview] = ToolOutputCut.preview(capped, cap: ToolOutputCut::WEB_PREVIEW_CHARS, tool: call[:name])
|
|
190
|
+
end
|
|
178
191
|
# Also here: a UI that missed the start (a replay gap) builds its row
|
|
179
192
|
# from this event (its title is in the activity).
|
|
180
193
|
completed.merge!(fields.slice(*ToolRowFields::EVENT_KEYS))
|
|
194
|
+
# The row's duration, the one every UI shows (live, a reload's tool
|
|
195
|
+
# record, the attached terminal): SessionMetrics' definition and
|
|
196
|
+
# rounding order, from the started emit to this one, less the wait.
|
|
197
|
+
completed[:duration_ms] = [((@clock.call - started_clock) * 1000).round - (waited_ms || 0), 0].max
|
|
181
198
|
emit(on_stream_event, completed)
|
|
182
199
|
# After the completed event: a hook's card (check-in, skills) prints
|
|
183
200
|
# under the call's tool row, not before it.
|
|
@@ -407,22 +424,25 @@ module Samagotchi
|
|
|
407
424
|
# The kernel's tools, for the activity line of a tool that isn't built in.
|
|
408
425
|
def tools = @kernel.tools
|
|
409
426
|
|
|
410
|
-
# The turn's Tools::ToolSurface (+context+'s
|
|
411
|
-
#
|
|
412
|
-
def surface(context) = Tools::ToolSurface.of(context
|
|
427
|
+
# The turn's Tools::ToolSurface (+context+'s; none raises
|
|
428
|
+
# Tools::MissingSurface).
|
|
429
|
+
def surface(context) = Tools::ToolSurface.of(context)
|
|
413
430
|
|
|
414
431
|
# The call its tool's entry unwraps it to (call_tool on a built-in),
|
|
415
432
|
# else the call as it is. On a host that loads tools, a direct call of
|
|
416
433
|
# a source's tool by its acts_as runs as the call_tool call first
|
|
417
434
|
# (Registry::Index#wrap): call_tool's row, rules and approvals. One
|
|
418
435
|
# that fails runs as it is: call_tool then refuses a built-in (fail
|
|
419
|
-
# closed).
|
|
436
|
+
# closed). A context without a surface raises Tools::MissingSurface
|
|
437
|
+
# here, before the gate: the call never runs.
|
|
420
438
|
def unwrapped(call, context)
|
|
421
439
|
return call if tools.nil? || call[:name].nil?
|
|
422
440
|
|
|
423
441
|
surface = surface(context)
|
|
424
442
|
call = wrapped(surface, call)
|
|
425
443
|
surface.declared_entry(call[:name])&.unwrap&.call(call, surface) || call
|
|
444
|
+
rescue Tools::MissingSurface
|
|
445
|
+
raise
|
|
426
446
|
rescue StandardError => e
|
|
427
447
|
Log.warn(:turn, "tool_unwrap_failed", tool: call[:name].to_s, error: "#{e.class}: #{e.message}")
|
|
428
448
|
call
|
|
@@ -115,7 +115,7 @@ module Samagotchi
|
|
|
115
115
|
raise ArgumentError, "#{NAME} #{verb} needs a tool name\n#{USAGE}" if tools.empty?
|
|
116
116
|
|
|
117
117
|
tools.reduce(toolkit) do |kit, tool|
|
|
118
|
-
raise ArgumentError, "no tool named #{tool} in this session" unless @engine.
|
|
118
|
+
raise ArgumentError, "no tool named #{tool} in this session" unless @engine.published_tools.key?(tool)
|
|
119
119
|
|
|
120
120
|
verb.casecmp?("add") ? kit.adding(tool) : kit.removing(tool)
|
|
121
121
|
end
|