samagotchi 0.153.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 +28 -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/bundles.md +9 -12
- data/docs/cli.md +80 -96
- data/docs/configuration.md +27 -26
- data/docs/context.md +3 -3
- data/docs/desktop.md +1 -1
- data/docs/guardrails.md +8 -10
- data/docs/hooks.md +9 -9
- data/docs/internals/attached-context.md +2 -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 +33 -7
- data/docs/internals/objectives.md +6 -5
- data/docs/internals/participants.md +7 -7
- data/docs/internals/prompt-caching.md +1 -1
- data/docs/internals/remote-sessions.md +7 -7
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +2 -2
- data/docs/plugins.md +23 -28
- data/docs/sessions.md +24 -24
- data/docs/sub-agent.md +2 -2
- data/docs/system-one.md +1 -1
- data/docs/testing.md +16 -6
- 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/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 +62 -2646
- 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/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/log.rb +7 -7
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/log_path.rb +1 -1
- 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_command.rb +2 -1
- 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/send_note.rb +2 -1
- 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/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 +1 -1
- data/lib/samagotchi/web/public/format.js +3 -3
- 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 +100 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- metadata +29 -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,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
|