samagotchi 0.152.0 → 0.154.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 +45 -1
- data/README.md +2 -3
- data/bin/chi +23 -24
- data/docs/architecture.md +30 -33
- data/docs/broadcast.md +5 -5
- data/docs/bundle-authoring.md +1 -1
- data/docs/bundles.md +93 -12
- data/docs/cli.md +83 -99
- data/docs/configuration.md +46 -40
- data/docs/context.md +3 -3
- data/docs/desktop.md +1 -1
- data/docs/guardrails.md +15 -15
- data/docs/hooks.md +13 -12
- data/docs/internals/anthropic.md +3 -3
- data/docs/internals/attached-context.md +5 -2
- data/docs/internals/background-tasks.md +3 -4
- data/docs/internals/block-renderers.md +4 -0
- data/docs/internals/bridge.md +3 -3
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +42 -12
- data/docs/internals/objectives.md +6 -5
- data/docs/internals/participants.md +8 -8
- data/docs/internals/prompt-caching.md +8 -4
- data/docs/internals/remote-sessions.md +7 -7
- data/docs/internals/web-frontend.md +20 -17
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +2 -2
- data/docs/plugins.md +52 -43
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +25 -25
- data/docs/sub-agent.md +2 -2
- data/docs/system-one.md +1 -1
- data/docs/testing.md +18 -8
- data/lib/samagotchi/answer_text.rb +34 -0
- data/lib/samagotchi/bridge.rb +43 -32
- data/lib/samagotchi/bridge_client.rb +10 -9
- data/lib/samagotchi/broadcast/recipients.rb +2 -2
- data/lib/samagotchi/broadcast/triage.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +5 -5
- data/lib/samagotchi/bundle_command/try/command.rb +1 -1
- data/lib/samagotchi/bundle_command/try/run.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
- data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/bundles/web-search/manifest.yml +13 -0
- data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
- data/lib/samagotchi/child_move.rb +3 -3
- data/lib/samagotchi/commands/registry.rb +6 -6
- data/lib/samagotchi/composer_route.rb +4 -5
- data/lib/samagotchi/config.rb +14 -12
- data/lib/samagotchi/context_candidate_choice.rb +1 -1
- data/lib/samagotchi/context_candidates.rb +4 -3
- data/lib/samagotchi/continue_offer.rb +1 -1
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/degrade.rb +75 -0
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
- data/lib/samagotchi/engine/cards.rb +87 -0
- data/lib/samagotchi/engine/context_judge.rb +138 -0
- data/lib/samagotchi/engine/events.rb +131 -0
- data/lib/samagotchi/engine/guardrails.rb +87 -0
- data/lib/samagotchi/engine/hooks.rb +119 -0
- data/lib/samagotchi/engine/messages.rb +124 -0
- data/lib/samagotchi/engine/model_settings.rb +127 -0
- data/lib/samagotchi/engine/models.rb +261 -0
- data/lib/samagotchi/engine/objectives.rb +116 -0
- data/lib/samagotchi/engine/participants.rb +86 -0
- data/lib/samagotchi/engine/plugins.rb +208 -0
- data/lib/samagotchi/engine/questions.rb +98 -0
- data/lib/samagotchi/engine/recap.rb +130 -0
- data/lib/samagotchi/engine/session.rb +131 -0
- data/lib/samagotchi/engine/side_requests.rb +135 -0
- data/lib/samagotchi/engine/steering.rb +134 -0
- data/lib/samagotchi/engine/toolkit.rb +269 -0
- data/lib/samagotchi/engine/turn_end.rb +180 -0
- data/lib/samagotchi/engine/turn_helpers.rb +160 -0
- data/lib/samagotchi/engine/verify.rb +153 -0
- data/lib/samagotchi/engine.rb +63 -2642
- data/lib/samagotchi/grants.rb +575 -0
- data/lib/samagotchi/guardrail_wiring.rb +2 -2
- data/lib/samagotchi/guardrails/context.rb +2 -3
- data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
- data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
- data/lib/samagotchi/guardrails/review_boundary.rb +4 -3
- data/lib/samagotchi/hook_routes.rb +3 -3
- data/lib/samagotchi/idle_recap.rb +4 -5
- data/lib/samagotchi/idle_reminders.rb +3 -4
- data/lib/samagotchi/iteration_limit.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +10 -11
- data/lib/samagotchi/launch_mode.rb +13 -32
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/llm_context_command.rb +3 -2
- data/lib/samagotchi/llm_context_stale.rb +2 -2
- data/lib/samagotchi/log.rb +7 -7
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/log_path.rb +1 -1
- data/lib/samagotchi/message_parts.rb +3 -2
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +18 -8
- data/lib/samagotchi/objectives.rb +24 -3
- data/lib/samagotchi/objectives_check.rb +8 -27
- data/lib/samagotchi/objectives_command.rb +4 -69
- data/lib/samagotchi/objectives_edit.rb +87 -0
- data/lib/samagotchi/output_formatter.rb +7 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/parent_answers_command.rb +5 -4
- data/lib/samagotchi/participant_exposure.rb +42 -0
- data/lib/samagotchi/participant_mode.rb +1 -1
- data/lib/samagotchi/participant_name.rb +1 -1
- data/lib/samagotchi/participants.rb +17 -17
- data/lib/samagotchi/participants_watch.rb +1 -1
- data/lib/samagotchi/pending_input_queue.rb +2 -2
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/plugin/context.rb +21 -7
- data/lib/samagotchi/plugin/peer.rb +2 -1
- data/lib/samagotchi/plugin/sessions.rb +6 -4
- data/lib/samagotchi/plugin_tasks.rb +1 -2
- data/lib/samagotchi/private_files.rb +29 -0
- data/lib/samagotchi/project_env.rb +2 -2
- data/lib/samagotchi/question_desk.rb +8 -7
- data/lib/samagotchi/recap_command.rb +2 -1
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/recap_store.rb +1 -1
- data/lib/samagotchi/redact.rb +86 -0
- data/lib/samagotchi/reminder_queue.rb +2 -2
- data/lib/samagotchi/remote_attach_command.rb +1 -1
- data/lib/samagotchi/reply_wait/poll.rb +2 -2
- data/lib/samagotchi/review_start.rb +1 -1
- data/lib/samagotchi/run_options.rb +6 -6
- data/lib/samagotchi/self_report.rb +42 -39
- data/lib/samagotchi/send_command/busy.rb +1 -1
- data/lib/samagotchi/send_command.rb +4 -1
- data/lib/samagotchi/session.rb +11 -2
- data/lib/samagotchi/session_access/contract.rb +3 -3
- data/lib/samagotchi/session_access/local.rb +9 -9
- data/lib/samagotchi/session_access/remote.rb +17 -7
- data/lib/samagotchi/session_access.rb +4 -4
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +8 -6
- data/lib/samagotchi/session_commands.rb +45 -48
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_delete_command.rb +4 -4
- data/lib/samagotchi/session_manager.rb +59 -36
- data/lib/samagotchi/session_metrics.rb +2 -2
- data/lib/samagotchi/session_model_settings.rb +3 -2
- data/lib/samagotchi/session_participants.rb +1 -1
- data/lib/samagotchi/session_retention.rb +3 -3
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +10 -10
- data/lib/samagotchi/steer.rb +1 -1
- data/lib/samagotchi/system_prompt.rb +102 -109
- data/lib/samagotchi/task_watch.rb +1 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
- data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
- data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
- data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
- data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
- data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
- data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
- data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
- data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
- data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
- data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
- data/lib/samagotchi/terminal_ui.rb +142 -1387
- data/lib/samagotchi/title_command.rb +2 -2
- data/lib/samagotchi/tool_runner.rb +14 -16
- data/lib/samagotchi/toolkit.rb +8 -5
- data/lib/samagotchi/toolkit_binding.rb +30 -5
- data/lib/samagotchi/toolkit_command.rb +2 -1
- data/lib/samagotchi/toolkit_report_command.rb +9 -5
- data/lib/samagotchi/toolkit_suggest.rb +12 -6
- data/lib/samagotchi/tools/builtin_calls.rb +14 -10
- data/lib/samagotchi/tools/deferred_tools.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +3 -1
- data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
- data/lib/samagotchi/tools/delegate_wait.rb +1 -1
- data/lib/samagotchi/tools/objectives_tool.rb +2 -2
- data/lib/samagotchi/tools/registry.rb +44 -20
- data/lib/samagotchi/tools/send_note.rb +2 -1
- data/lib/samagotchi/tools/tool_index.rb +27 -21
- data/lib/samagotchi/turn_endings.rb +2 -2
- data/lib/samagotchi/turn_flow.rb +4 -5
- data/lib/samagotchi/update_hint.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +39 -50
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_review.rb +5 -5
- data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
- data/lib/samagotchi/web/participant_view.rb +2 -82
- data/lib/samagotchi/web/participants_admin.rb +1 -1
- data/lib/samagotchi/web/public/app.js +61 -357
- data/lib/samagotchi/web/public/block_host.js +35 -8
- data/lib/samagotchi/web/public/block_renderers.css +9 -5
- data/lib/samagotchi/web/public/chains.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer_mode.js +110 -0
- data/lib/samagotchi/web/public/format.js +3 -3
- data/lib/samagotchi/web/public/owner_claim.js +81 -0
- data/lib/samagotchi/web/public/owner_verify.js +112 -0
- data/lib/samagotchi/web/public/session_fields.js +3 -3
- data/lib/samagotchi/web/public/strip_grip.js +3 -3
- data/lib/samagotchi/web/public/strip_hidden.js +65 -0
- data/lib/samagotchi/web/public/version_notices.js +132 -0
- data/lib/samagotchi/web/push/store.rb +1 -1
- data/lib/samagotchi/web/session_access_routes.rb +5 -4
- data/lib/samagotchi/web/session_control.rb +12 -8
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +2 -2
- data/lib/samagotchi/web/session_show.rb +2 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +106 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- metadata +36 -6
- data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
- data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
- data/lib/samagotchi/web/grants.rb +0 -575
- data/lib/samagotchi/web/participant_exposure.rb +0 -44
- data/lib/samagotchi/web/private_files.rb +0 -31
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# The event log: persistent observers, #announce (ANNOUNCEABLE_EVENTS),
|
|
6
|
+
# the event lock, held announcements, the load events, and the stream
|
|
7
|
+
# and emit helpers (EventFanout's, forwarded). Writes
|
|
8
|
+
# @guardrail_failures_announced; reads @session_observer, @event_fanout,
|
|
9
|
+
# @extension_load, @guardrail_failures, @plugin_failures.
|
|
10
|
+
module EventsPart
|
|
11
|
+
# Subscribe a persistent observer to engine events.
|
|
12
|
+
#
|
|
13
|
+
# Unlike the turn-scoped `on_event:` sink, a subscribed observer keeps
|
|
14
|
+
# receiving events across every `run_turn` call on this Engine. Each
|
|
15
|
+
# delivery carries a locally-monotonic `event_seq`. The returned handle can
|
|
16
|
+
# be used to unsubscribe later.
|
|
17
|
+
# @param observer [#call] receives event hashes (with `event_seq:` merged in)
|
|
18
|
+
# @return [Samagotchi::SessionObserver::SubscribedObserver] handle to unsubscribe
|
|
19
|
+
def subscribe(observer:)
|
|
20
|
+
@session_observer.subscribe(observer: observer)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Unsubscribe a previously-registered observer.
|
|
24
|
+
# @param handle [Samagotchi::SessionObserver::SubscribedObserver]
|
|
25
|
+
# @return [Boolean] whether the observer was removed (nil/unknown never raises)
|
|
26
|
+
def unsubscribe(handle:)
|
|
27
|
+
@session_observer.unsubscribe(handle: handle)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Integer] total engine events emitted so far (locally monotonic)
|
|
31
|
+
def event_count
|
|
32
|
+
@session_observer.event_count
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Put a transport-level event into the ordered event log. Unlike turn
|
|
36
|
+
# events it reaches only persistent observers (no turn sink, no memory
|
|
37
|
+
# capture).
|
|
38
|
+
# @param event [Hash] with :type in ANNOUNCEABLE_EVENTS
|
|
39
|
+
# @raise [ArgumentError] for any other type
|
|
40
|
+
def announce(event)
|
|
41
|
+
unless ANNOUNCEABLE_EVENTS.include?(event[:type])
|
|
42
|
+
raise ArgumentError, "cannot announce #{event[:type].inspect} (allowed: #{ANNOUNCEABLE_EVENTS.join(", ")})"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@session_observer.notify(event)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Run the block with the event log held: no event is numbered or
|
|
49
|
+
# delivered meanwhile, and events the block emits keep their order.
|
|
50
|
+
def synchronize_events(&)
|
|
51
|
+
@session_observer.synchronize(&)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The working set's :working_set_updated, announced between turns
|
|
55
|
+
# (after a command changed what the next request sends), when it
|
|
56
|
+
# differs from the last one emitted (#emit_working_set). Under the
|
|
57
|
+
# event lock, so it is ordered with the change's own events.
|
|
58
|
+
def announce_working_set(session = @session)
|
|
59
|
+
return unless session
|
|
60
|
+
|
|
61
|
+
synchronize_events { emit_working_set(nil, session, session.messages) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# ── Load events ───────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
# Announce what failed to load and what plugins showed while loading,
|
|
67
|
+
# once, as soon as the owner can show it (the worker's Bridge is up):
|
|
68
|
+
# the guardrail and plugin warnings, an orphaned
|
|
69
|
+
# project memory folder (label: "memory", MemoryOrphans), then the
|
|
70
|
+
# held notices (between_turns) and cards (in_turn: false, so late
|
|
71
|
+
# joiners get them). Without this call the first turn announces them
|
|
72
|
+
# (#announce_guardrail_failures).
|
|
73
|
+
def announce_load_events!
|
|
74
|
+
synchronize_events do
|
|
75
|
+
next if @guardrail_failures_announced
|
|
76
|
+
|
|
77
|
+
@guardrail_failures_announced = true
|
|
78
|
+
message = @guardrail_failures.message
|
|
79
|
+
announce({ type: :guardrail_warning, message: message }) if message
|
|
80
|
+
plugins = @plugin_failures.message
|
|
81
|
+
announce({ type: :guardrail_warning, message: plugins, label: "plugins" }) if plugins
|
|
82
|
+
memory = MemoryOrphans.message
|
|
83
|
+
announce({ type: :guardrail_warning, message: memory, label: "memory" }) if memory
|
|
84
|
+
@extension_load.held_events.each do |event|
|
|
85
|
+
announce(event[:type] == :card ? event.merge(in_turn: false) : event.merge(between_turns: true))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Run the block holding back the cards and between-turns notices it
|
|
92
|
+
# announces on this thread, so the caller announces them after its own
|
|
93
|
+
# event (a worker's command: its command_ran first, so a UI prints the
|
|
94
|
+
# command's output before its cards).
|
|
95
|
+
# @return [Array(Object, Array<Hash>)] the block's value and the held events
|
|
96
|
+
def holding_announcements
|
|
97
|
+
key = :"samagotchi_held_#{object_id}"
|
|
98
|
+
outer = Thread.current[key]
|
|
99
|
+
Thread.current[key] = []
|
|
100
|
+
value = yield
|
|
101
|
+
[value, Thread.current[key]]
|
|
102
|
+
ensure
|
|
103
|
+
Thread.current[key] = outer
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Announce +event+ now, or keep it for #holding_announcements' caller.
|
|
107
|
+
def announce_or_hold(event)
|
|
108
|
+
held = Thread.current[:"samagotchi_held_#{object_id}"]
|
|
109
|
+
held ? held << event : announce(event)
|
|
110
|
+
end
|
|
111
|
+
private :announce_or_hold
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# ── Event helpers ──────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
# The stream handler, the working set and the event fan-out are
|
|
118
|
+
# EventFanout's (#build_event_fanout); these forward to it.
|
|
119
|
+
def build_stream_event_handler(...) = @event_fanout.build_stream_event_handler(...)
|
|
120
|
+
def emit_working_set(...) = @event_fanout.emit_working_set(...)
|
|
121
|
+
private :emit_working_set
|
|
122
|
+
def working_set_llm_context = @event_fanout.working_set_llm_context
|
|
123
|
+
private :working_set_llm_context
|
|
124
|
+
def with_generation_report(event) = @event_fanout.with_generation_report(event)
|
|
125
|
+
private :with_generation_report
|
|
126
|
+
def stream_watch(cancel_controller) = @event_fanout.stream_watch(cancel_controller)
|
|
127
|
+
private :stream_watch
|
|
128
|
+
def emit_event(on_event, event) = @event_fanout.emit_event(on_event, event)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# The tool guardrails (GuardrailWiring, forwarded): who answers an
|
|
6
|
+
# approval, the rules and approvals, their state dir, and the load
|
|
7
|
+
# warnings the first turn announces. Writes @state_dir,
|
|
8
|
+
# @guardrail_failures_announced; reads @guardrail_wiring,
|
|
9
|
+
# @guardrail_failures, @plugin_failures, @hook_routes.
|
|
10
|
+
module GuardrailsPart
|
|
11
|
+
# ── Guardrails ─────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
# Who can answer an approval: :worker or :non_interactive (the
|
|
14
|
+
# default, so a bare Engine denies instead of waiting for nobody). Set
|
|
15
|
+
# by the host (TerminalUI, Worker).
|
|
16
|
+
def interface
|
|
17
|
+
@guardrail_wiring.interface
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def interface=(value)
|
|
21
|
+
@guardrail_wiring.interface = value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def guardrail_state_dir=(state_dir)
|
|
25
|
+
# Also where list_sessions and send_note look for other sessions.
|
|
26
|
+
@state_dir = state_dir
|
|
27
|
+
@guardrail_wiring.state_dir = state_dir
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The YAML rules, read again when a file changed (GuardrailWiring#rules).
|
|
31
|
+
# @return [Guardrails::Rules]
|
|
32
|
+
def guardrail_rules
|
|
33
|
+
@guardrail_wiring.rules
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Guardrails::LoadFailures]
|
|
37
|
+
attr_reader :guardrail_failures
|
|
38
|
+
|
|
39
|
+
# @return [Guardrails::Approvals]
|
|
40
|
+
def guardrail_approvals
|
|
41
|
+
@guardrail_wiring.approvals
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Guardrails::LoadFailures] the plugins that failed to load
|
|
45
|
+
attr_reader :plugin_failures
|
|
46
|
+
|
|
47
|
+
# Once per Engine, on its first turn: what failed to load, so every UI
|
|
48
|
+
# (the attached TUI, the web, a one-shot's stderr) shows it: the guardrails, then the plugins
|
|
49
|
+
# (label: "plugins"; the UIs say guardrails without one), an orphaned
|
|
50
|
+
# project memory folder (label: "memory"), then the notices and cards plugins showed as they loaded.
|
|
51
|
+
def announce_guardrail_failures(on_event)
|
|
52
|
+
return if @guardrail_failures_announced
|
|
53
|
+
|
|
54
|
+
@guardrail_failures_announced = true
|
|
55
|
+
message = @guardrail_failures.message
|
|
56
|
+
emit_event(on_event, { type: :guardrail_warning, message: message }) if message
|
|
57
|
+
plugins = @plugin_failures.message
|
|
58
|
+
emit_event(on_event, { type: :guardrail_warning, message: plugins, label: "plugins" }) if plugins
|
|
59
|
+
memory = MemoryOrphans.message
|
|
60
|
+
emit_event(on_event, { type: :guardrail_warning, message: memory, label: "memory" }) if memory
|
|
61
|
+
@extension_load.held_events.each { |event| emit_event(on_event, event) }
|
|
62
|
+
end
|
|
63
|
+
private :announce_guardrail_failures
|
|
64
|
+
|
|
65
|
+
# The warning the first turn announced (nil before it, or with nothing
|
|
66
|
+
# failed), for a UI that joins later (Bridge#snapshot).
|
|
67
|
+
def guardrail_warning
|
|
68
|
+
@guardrail_failures.message if @guardrail_failures_announced
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The plugins' load warning the first turn announced, likewise, and what
|
|
72
|
+
# their failed chi.on handlers told (HookRoutes#plugin_hook_failed).
|
|
73
|
+
def plugin_warning
|
|
74
|
+
loaded = @plugin_failures.message if @guardrail_failures_announced
|
|
75
|
+
[loaded, @hook_routes.hook_failures_message].compact.join("; ").then { |text| text unless text.empty? }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Ask the user to approve a call the gate voted `ask` on
|
|
79
|
+
# (GuardrailWiring#request_approval).
|
|
80
|
+
# @param verdict [Guardrails::Verdict]
|
|
81
|
+
# @return [Guardrails::Verdict]
|
|
82
|
+
def request_approval(verdict)
|
|
83
|
+
@guardrail_wiring.request_approval(verdict)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# Hooks and reminders: turn-scoped hooks (register, clear), the hook
|
|
6
|
+
# runtime's notify/ask/stop (HookRoutes, forwarded), the copy of the
|
|
7
|
+
# conversation a hook reads, and the due reminders. Reads @hooks,
|
|
8
|
+
# @hook_routes, @reminder_queue, @reminder_store.
|
|
9
|
+
module HooksPart
|
|
10
|
+
# @return [Array<String>] reminder names queued for a reminder turn
|
|
11
|
+
def due_reminder_names
|
|
12
|
+
@reminder_queue.pending_names
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Queue reminder names for a reminder turn (IdleReminders callback).
|
|
16
|
+
def note_due_reminders(names)
|
|
17
|
+
@reminder_queue.note_pending(names)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def clear_due_reminder_names!
|
|
21
|
+
@reminder_queue.clear_pending!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# ── Hooks API ──────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
# Register a hook callback for a named lifecycle event.
|
|
27
|
+
#
|
|
28
|
+
# Hooks are turn-scoped: they are automatically cleared after each
|
|
29
|
+
# `run_turn` call so that a single turn's hooks do not leak into the next.
|
|
30
|
+
#
|
|
31
|
+
# A hook that raises tells the user once, as a bundle's does
|
|
32
|
+
# (HookRoutes#plugin_hook_failed); the registry logs each failure and goes on.
|
|
33
|
+
#
|
|
34
|
+
# @param name [Symbol] one of the hook names (see Hooks module)
|
|
35
|
+
# @param block [Proc] receives an event hash (may mutate in place)
|
|
36
|
+
# @return [void]
|
|
37
|
+
def register_hook(name, &block)
|
|
38
|
+
return @hooks.register(name) unless block
|
|
39
|
+
|
|
40
|
+
@hooks.register(name) do |event|
|
|
41
|
+
yield(event)
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
@hook_routes.plugin_hook_failed("#{Hooks::Registry::TURN_HOOK_LABEL} (#{name})", "#{e.class}: #{e.message}")
|
|
44
|
+
raise
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Unregister a previously registered hook.
|
|
49
|
+
# @param name [Symbol]
|
|
50
|
+
# @return [Boolean] true if it was removed, false if not found
|
|
51
|
+
def unregister_hook(name)
|
|
52
|
+
@hooks.unregister(name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Clear all registered hooks. Called automatically at the end of each
|
|
56
|
+
# `run_turn` to keep hooks turn-scoped.
|
|
57
|
+
# @return [void]
|
|
58
|
+
def clear_hooks
|
|
59
|
+
@hooks.clear_all
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# ── Reminders API ────────────────────────────────────────────────────────────
|
|
63
|
+
#
|
|
64
|
+
# Each turn injects the reminders due then (#turn_messages,
|
|
65
|
+
# ReminderQueue#inject!). Between turns the idle tick (IdleReminders)
|
|
66
|
+
# finds them due and its callback queues their names
|
|
67
|
+
# (#note_due_reminders); the worker runs a reminder turn
|
|
68
|
+
# (a continue turn) for them.
|
|
69
|
+
|
|
70
|
+
# @return [Boolean] whether any reminder is due now (the store's view,
|
|
71
|
+
# which a turn would inject), regardless of the queued names
|
|
72
|
+
def reminders_due?
|
|
73
|
+
@reminder_queue.due?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [ReminderStore] the reminder store for inspection
|
|
77
|
+
attr_reader :reminder_store
|
|
78
|
+
|
|
79
|
+
# The conversation as a hook may read it: a frozen array of copied
|
|
80
|
+
# messages, so a hook cannot change what the turn sends or stores.
|
|
81
|
+
def hook_messages(messages)
|
|
82
|
+
AnswerDisplay.strip_all(messages).map(&:dup).freeze
|
|
83
|
+
end
|
|
84
|
+
private :hook_messages
|
|
85
|
+
|
|
86
|
+
# ── The hook runtime (HookRoutes) ───────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
# What a hook (Hooks::Runtime) and a plugin (Plugin::Host) can do beyond
|
|
89
|
+
# reading its event, routed to the UIs by HookRoutes; these forward.
|
|
90
|
+
def hook_runtime = @hook_routes.runtime
|
|
91
|
+
private :hook_runtime
|
|
92
|
+
|
|
93
|
+
def hook_source(hook) = @hook_routes.source(hook)
|
|
94
|
+
private :hook_source
|
|
95
|
+
|
|
96
|
+
# One line to the user (:hook_notice), HookRoutes#notify.
|
|
97
|
+
def hook_notify(text, level, hook, fallback_for: nil, extra: {})
|
|
98
|
+
@hook_routes.notify(text, level, hook, fallback_for: fallback_for, extra: extra)
|
|
99
|
+
end
|
|
100
|
+
private :hook_notify
|
|
101
|
+
|
|
102
|
+
def guardrail_notify(message) = @hook_routes.guardrail_notify(message)
|
|
103
|
+
private :guardrail_notify
|
|
104
|
+
|
|
105
|
+
# @param question [HookQuestion]
|
|
106
|
+
# @return [Hash, nil] {selected:, freeform:, selected_indices:}
|
|
107
|
+
def hook_ask_user(question) = @hook_routes.ask_user(question)
|
|
108
|
+
private :hook_ask_user
|
|
109
|
+
|
|
110
|
+
# @return [Boolean] true when a running turn was cancelled now
|
|
111
|
+
def hook_stop_turn(reason, hook) = @hook_routes.stop_turn(reason, hook)
|
|
112
|
+
private :hook_stop_turn
|
|
113
|
+
|
|
114
|
+
# @return [Boolean] true when a streaming generation was cut now
|
|
115
|
+
def hook_stop_generation(reason, hook) = @hook_routes.stop_generation(reason, hook)
|
|
116
|
+
private :hook_stop_generation
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# The session messages API: out-of-turn edits to the current session's
|
|
6
|
+
# conversation (replace, append, a context note, rollback), its
|
|
7
|
+
# checkpoints and last answer, and the running turn's live conversation.
|
|
8
|
+
# Reads @session and @kernel; every edit goes through
|
|
9
|
+
# #replace_session_messages.
|
|
10
|
+
module MessagesPart
|
|
11
|
+
# The strategy the working set is built under now, as LLMContextView
|
|
12
|
+
# takes it: the running turn's, else the next turn's (#emit_working_set).
|
|
13
|
+
# /working-set's ops act under it.
|
|
14
|
+
# @return [Symbol, Array<Symbol>]
|
|
15
|
+
def working_set_strategy
|
|
16
|
+
working_set_llm_context&.strategy || LLMContextView::NONE
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Replace the current session's conversation, between turns (a
|
|
20
|
+
# /working-set op's copy: #clone_messages, then its edits).
|
|
21
|
+
# @param messages [Array<Hash>]
|
|
22
|
+
# @return [Array<Hash>] the session's messages
|
|
23
|
+
def replace_messages(messages)
|
|
24
|
+
raise ArgumentError, "no current session" unless @session
|
|
25
|
+
|
|
26
|
+
replace_session_messages(@session, messages)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Add a context note to the session's conversation, between turns: a
|
|
30
|
+
# tail system message the next turn sees, announced as :context_added.
|
|
31
|
+
# The array is replaced, not mutated, and the append and the event
|
|
32
|
+
# happen under the event lock, so a joining UI sees both or neither.
|
|
33
|
+
# The caller saves the session.
|
|
34
|
+
# @param note [Hash] SessionInbox.read_note's shape
|
|
35
|
+
# @return [Hash, nil] the message, or nil when the conversation already
|
|
36
|
+
# holds this note (a note file re-read after a crash)
|
|
37
|
+
def add_context_note(session, note)
|
|
38
|
+
synchronize_events do
|
|
39
|
+
next nil if Array(session.messages).any? { |m| m[:note_id] == note[:note_id] }
|
|
40
|
+
|
|
41
|
+
message = ContextNote.message(**note)
|
|
42
|
+
entry = note[:objectives] && Objectives.entry(note[:objectives], before: Objectives.build(session.messages))
|
|
43
|
+
replace_session_messages(session, Array(session.messages) + [message])
|
|
44
|
+
announce({ type: :context_added, session_id: session.id, note_id: note[:note_id], source: note[:source],
|
|
45
|
+
label: ContextNote.label_of(message), from_session: note[:from_session],
|
|
46
|
+
from_cwd: note[:from_cwd], text: note[:text], context_source: note[:context_source],
|
|
47
|
+
created_at: note[:created_at], objectives: entry }.compact)
|
|
48
|
+
announce_objectives(session) if entry
|
|
49
|
+
objectives_audit.note(session, note)
|
|
50
|
+
message
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# ── Session messages API ───────────────────────────────────────────────
|
|
55
|
+
#
|
|
56
|
+
# Out-of-turn edits to the current session's conversation (`!cmd` output,
|
|
57
|
+
# rollback after Ctrl-C). Each replaces the array rather than mutating it,
|
|
58
|
+
# so the recap's lock-free snapshot never sees a half-applied edit.
|
|
59
|
+
|
|
60
|
+
# The answer a UI shows last (AnswerTail), a copy of that one message
|
|
61
|
+
# only: walked back from the end, never a #messages_checkpoint of the
|
|
62
|
+
# whole conversation (the Bridge's GET tail). +turn_id+: that turn's
|
|
63
|
+
# answer (AnswerTail.find); unknown or nil: the newest.
|
|
64
|
+
# @return [Hash, nil]
|
|
65
|
+
def last_answer_message(turn_id: nil)
|
|
66
|
+
AnswerTail.find(@session&.messages, turn_id: turn_id)&.dup
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @return [Array<Hash>] a copy of the current session's messages to hand
|
|
70
|
+
# back to #rollback_to later
|
|
71
|
+
def messages_checkpoint
|
|
72
|
+
clone_messages(@session&.messages)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The running turn's conversation as its loop builds it (both loops
|
|
76
|
+
# register it on the kernel's llm_context_turn): the same array the loop
|
|
77
|
+
# appends to, not a copy, so read it on the turn's thread (the pending
|
|
78
|
+
# input drain, at an iteration boundary: TurnCheckpoint), or from another
|
|
79
|
+
# only as a dup, read only (/objectives). nil when no
|
|
80
|
+
# turn runs (llm_context_turn is never cleared, and still holds the
|
|
81
|
+
# last turn's then).
|
|
82
|
+
# @return [Array<Hash>, nil]
|
|
83
|
+
def live_conversation
|
|
84
|
+
return nil unless turn_running? && @kernel.respond_to?(:llm_context_turn)
|
|
85
|
+
|
|
86
|
+
@kernel.llm_context_turn&.conversation
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Append messages to the current session's conversation.
|
|
90
|
+
# @param messages [Array<Hash>]
|
|
91
|
+
# @return [Array<Hash>] the session's messages
|
|
92
|
+
def append_messages(messages)
|
|
93
|
+
raise ArgumentError, "no current session" unless @session
|
|
94
|
+
|
|
95
|
+
replace_session_messages(@session, Array(@session.messages) + clone_messages(messages))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Restore the current session's conversation to a #messages_checkpoint.
|
|
99
|
+
# Between turns the working set is announced when that changed it (a
|
|
100
|
+
# !rollback or a canceled continue dropping the turn's tool calls, the
|
|
101
|
+
# set emptied too), so the page doesn't keep the dropped items until
|
|
102
|
+
# the next request.
|
|
103
|
+
# @param checkpoint [Array<Hash>]
|
|
104
|
+
# @return [Array<Hash>] the session's messages
|
|
105
|
+
def rollback_to(checkpoint)
|
|
106
|
+
raise ArgumentError, "no current session" unless @session
|
|
107
|
+
|
|
108
|
+
messages = replace_session_messages(@session, clone_messages(checkpoint))
|
|
109
|
+
unless turn_running?
|
|
110
|
+
announce_working_set
|
|
111
|
+
announce_objectives
|
|
112
|
+
end
|
|
113
|
+
messages
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Clone a messages array (shallow dup of each element).
|
|
117
|
+
# @param messages [Array<Hash>]
|
|
118
|
+
# @return [Array<Hash>]
|
|
119
|
+
def clone_messages(messages)
|
|
120
|
+
Array(messages).map(&:dup)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# The session's settings on its effective model (SessionModelSettings) and
|
|
6
|
+
# the UIs' snapshots (EngineSnapshots), forwarded: /model's and /stats'
|
|
7
|
+
# summaries, the overrides a session saves, what a turn reads. Writes
|
|
8
|
+
# @last_context_status; reads @model_settings, @engine_snapshots, @metrics.
|
|
9
|
+
module ModelSettingsPart
|
|
10
|
+
# The metrics for /stats, which may make one short /props GET
|
|
11
|
+
# (EngineSnapshots#stats_snapshot).
|
|
12
|
+
# @return [Hash] @metrics.snapshot, filled in
|
|
13
|
+
def stats_snapshot = @engine_snapshots.stats_snapshot
|
|
14
|
+
|
|
15
|
+
# ── The session's model settings (SessionModelSettings) ──────────────────
|
|
16
|
+
|
|
17
|
+
# The session's own thinking, reasoning_history and llm_context values
|
|
18
|
+
# over the effective model's, the summaries /model and /stats show, and
|
|
19
|
+
# the saved context's status: SessionModelSettings' (resolved on every
|
|
20
|
+
# read); these forward.
|
|
21
|
+
def saved_context_status(context, model: :current) = @model_settings.saved_context_status(context, model: model)
|
|
22
|
+
def llm_context_explained(own: :session) = @model_settings.llm_context_explained(own: own)
|
|
23
|
+
def llm_context_summary = @model_settings.llm_context_summary
|
|
24
|
+
def llm_context_override = @model_settings.llm_context_override
|
|
25
|
+
def chat_model? = @model_settings.chat_model?
|
|
26
|
+
def sampling_summary = @model_settings.sampling_summary
|
|
27
|
+
def thinking_summary = @model_settings.thinking_summary
|
|
28
|
+
def steer_cut_after_explained = @model_settings.steer_cut_after_explained
|
|
29
|
+
def steer_cut_after = @model_settings.steer_cut_after
|
|
30
|
+
def steer_cut_summary = @model_settings.steer_cut_summary
|
|
31
|
+
def thinking_explained(own: thinking_override) = @model_settings.thinking_explained(own: own)
|
|
32
|
+
def reasoning_history_summary = @model_settings.reasoning_history_summary
|
|
33
|
+
def reasoning_history_cache_cost(from, to) = @model_settings.reasoning_history_cache_cost(from, to)
|
|
34
|
+
def reasoning_history_override = @model_settings.reasoning_history_override
|
|
35
|
+
def thinking_override = @model_settings.thinking_override
|
|
36
|
+
def served_expected_by(served) = @model_settings.served_expected_by(served)
|
|
37
|
+
def served_expected?(served) = @model_settings.served_expected?(served)
|
|
38
|
+
|
|
39
|
+
def reasoning_history_explained(native: false, own: reasoning_history_override)
|
|
40
|
+
@model_settings.reasoning_history_explained(native: native, own: own)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Set the session's own reasoning_history or thinking level (nil unsets
|
|
44
|
+
# it): SessionModelSettings'. The caller saves the session.
|
|
45
|
+
# @raise [ArgumentError] with no session yet
|
|
46
|
+
def reasoning_history_override=(level)
|
|
47
|
+
@model_settings.reasoning_history_override = level
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def thinking_override=(level)
|
|
51
|
+
@model_settings.thinking_override = level
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Set the session's own llm_context values (an empty override unsets
|
|
55
|
+
# them); the next turn's start resolves them, never a running one (its
|
|
56
|
+
# TurnSettings are fixed). The caller saves the session.
|
|
57
|
+
# @raise [ArgumentError] with no session yet
|
|
58
|
+
def llm_context_override=(override)
|
|
59
|
+
raise ArgumentError, "no session yet" unless @session
|
|
60
|
+
|
|
61
|
+
SessionSetting::LLM_CONTEXT.set(@session, override)
|
|
62
|
+
# The status a later client reads counts against the new budget.
|
|
63
|
+
@last_context_status = saved_context_status(@metrics.snapshot[:context]) || @last_context_status if @last_context_status
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Whether the session has a model reply (a cache to restart at all).
|
|
67
|
+
def history?
|
|
68
|
+
Array(@session&.messages).any? { |entry| entry[:role].to_s == "model" }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The latest known context size used by a request, for a cache-restart
|
|
72
|
+
# confirmation; nil before any request reports a count.
|
|
73
|
+
# @return [Integer, nil]
|
|
74
|
+
def cache_restart_tokens
|
|
75
|
+
used = @metrics.snapshot.dig(:context, :used_tokens)
|
|
76
|
+
used if used.is_a?(Integer)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Whether a cache restart can invalidate preserved thinking blocks.
|
|
80
|
+
def cache_restart_anthropic?
|
|
81
|
+
effective_model.entry&.api.to_s == "anthropic"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# What a thinking level change costs the effective model's prompt
|
|
85
|
+
# cache (Thinking.cache_cost): :full, :provider or :tail.
|
|
86
|
+
# @return [Symbol]
|
|
87
|
+
def thinking_cache_cost
|
|
88
|
+
effective_model.thinking_cache_cost { profile }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The model the server serves for the current model, and the name asked
|
|
92
|
+
# for: what the last generation of that name reported, else llama.cpp's
|
|
93
|
+
# model_alias (/props, one short cached probe; not with probe: false),
|
|
94
|
+
# else [nil, nil].
|
|
95
|
+
# @return [Array(String, String), Array(nil, nil)]
|
|
96
|
+
def served_model(probe: true)
|
|
97
|
+
@engine_snapshots.served_model_for(@metrics.served_report, model: probe ? effective_model : nil)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Read-only snapshot of the engine's view of the current session plus the
|
|
101
|
+
# live event sequence; cheap, never probes, never raises
|
|
102
|
+
# (EngineSnapshots#session_state_snapshot has its keys).
|
|
103
|
+
# @return [Hash]
|
|
104
|
+
def session_state_snapshot = @engine_snapshots.session_state_snapshot
|
|
105
|
+
|
|
106
|
+
# The effective model's sampling and thinking level as the next turn
|
|
107
|
+
# reads them (SessionModelSettings).
|
|
108
|
+
def turn_sampling = @model_settings.turn_sampling
|
|
109
|
+
def turn_thinking = @model_settings.turn_thinking
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
# The effective model's window_tokens and price, read per turn
|
|
114
|
+
# (SessionModelSettings).
|
|
115
|
+
def turn_window_setting = @model_settings.turn_window_setting
|
|
116
|
+
def turn_price = @model_settings.turn_price
|
|
117
|
+
|
|
118
|
+
# The snapshots' helpers are EngineSnapshots' (#build_engine_snapshots);
|
|
119
|
+
# these forward to it.
|
|
120
|
+
def served_model_for(...) = @engine_snapshots.served_model_for(...)
|
|
121
|
+
def kit_prompt_stats = @engine_snapshots.kit_prompt_stats
|
|
122
|
+
|
|
123
|
+
# +settings+' chips (EngineSnapshots#setting_chips).
|
|
124
|
+
def setting_chips(settings, model) = @engine_snapshots.setting_chips(settings, model)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|