samagotchi 0.153.0 → 0.154.1
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 +42 -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 +82 -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 +21 -8
- 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 +4 -2
- data/docs/internals/remote-sessions.md +7 -7
- data/docs/internals/web-frontend.md +2 -2
- 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 +31 -6
- data/lib/samagotchi/answer_text.rb +33 -0
- data/lib/samagotchi/bridge/card_places.rb +56 -7
- data/lib/samagotchi/bridge/card_store.rb +24 -9
- data/lib/samagotchi/bridge.rb +52 -32
- data/lib/samagotchi/bridge_client.rb +19 -14
- 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/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -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/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +3 -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 +3 -3
- 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/engine_snapshots.rb +9 -8
- 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 +7 -6
- data/lib/samagotchi/idle_reminders.rb +3 -4
- data/lib/samagotchi/iteration_limit.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +13 -13
- data/lib/samagotchi/launch_mode.rb +13 -32
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/llm/chat_loop.rb +3 -1
- 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/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 +67 -49
- 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 +27 -7
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +10 -10
- data/lib/samagotchi/steer.rb +13 -2
- data/lib/samagotchi/stream_event.rb +22 -0
- data/lib/samagotchi/system_prompt.rb +154 -145
- 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 +1 -11
- 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 +17 -22
- data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
- 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 +2 -12
- data/lib/samagotchi/terminal_ui/status_row.rb +6 -14
- data/lib/samagotchi/terminal_ui.rb +142 -1387
- data/lib/samagotchi/title_command.rb +2 -2
- data/lib/samagotchi/tool_runner.rb +15 -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 +11 -7
- data/lib/samagotchi/update_hint.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +42 -51
- data/lib/samagotchi/web/message_display.rb +7 -0
- 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/card_stamps.js +27 -0
- data/lib/samagotchi/web/public/cards_place.js +5 -3
- 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 +4 -3
- data/lib/samagotchi/web/public/turn_html.js +8 -4
- data/lib/samagotchi/web/push/store.rb +1 -1
- data/lib/samagotchi/web/session_access_routes.rb +13 -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 +6 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +103 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- data/lib/samagotchi.rb +0 -1
- metadata +31 -7
- data/lib/samagotchi/pending_input_queue.rb +0 -55
- 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,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
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# The model: its names and refs, identity and notes, /model's switch (and
|
|
6
|
+
# the context it shows after), the profile (ModelBinding's, forwarded) and
|
|
7
|
+
# the loop it runs on. Writes @client, @default_model_name,
|
|
8
|
+
# @last_context_status, @chat_backend; reads @model_binding, @host_registry,
|
|
9
|
+
# @kernel, @metrics, @prompt_builder.
|
|
10
|
+
module ModelsPart
|
|
11
|
+
# The backend for the next turn: the loop the effective model's host speaks.
|
|
12
|
+
def backend(model = effective_model)
|
|
13
|
+
backend_for(model)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The model as given (maybe an alias; HostRegistry#resolve applies it).
|
|
17
|
+
def effective_model_name = @model_binding.choice.name
|
|
18
|
+
|
|
19
|
+
# The model as typed at start or at the last /model.
|
|
20
|
+
def typed_model_name = @model_binding.choice.typed
|
|
21
|
+
|
|
22
|
+
# The resolved ref a session stores for the effective model (ModelRef#ref:
|
|
23
|
+
# the alias applied, "host:id" when it names a host).
|
|
24
|
+
def effective_model_ref
|
|
25
|
+
model_ref_for(effective_model_name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The effective model as the system prompt names it (SystemPrompt::ModelIdentity):
|
|
29
|
+
# the ref, its host, the overlay key and, for a local llama.cpp host
|
|
30
|
+
# only, the model its /props names when that differs. Never a new
|
|
31
|
+
# request: only the /props answer profile resolution already cached.
|
|
32
|
+
def model_identity
|
|
33
|
+
model = effective_model
|
|
34
|
+
entry = model.entry
|
|
35
|
+
host = entry && "host #{entry.name}, #{entry.url || "#{entry.host}:#{entry.port}"}"
|
|
36
|
+
SystemPrompt::ModelIdentity.new(ref: model.ref, host: host, key: @model_binding.model_key,
|
|
37
|
+
served: model.prompt_served, fallback_key: @model_binding.fallback_model_key)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The model notes for the effective model (ModelNotes): matched on its
|
|
41
|
+
# bare id and key, the session's mutes left out.
|
|
42
|
+
# @return [Array<ModelNotes::Note>]
|
|
43
|
+
def model_notes
|
|
44
|
+
ModelNotes.for(name: guardrail_model_name, key: @model_binding.model_key, fallback_key: @model_binding.fallback_model_key,
|
|
45
|
+
muted: @muted_memory_names)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The model notes the session's prompt carried at its last build
|
|
49
|
+
# (Session#prompt_notes): a resumed session's or a woken worker's saved
|
|
50
|
+
# ones until this process builds its prompt; a fresh session (no build
|
|
51
|
+
# yet, no messages) has none saved, so the effective model's prompt
|
|
52
|
+
# would load them: the notes #model_notes names, as without a session.
|
|
53
|
+
# The three readers (/model, /stats, the web) share this.
|
|
54
|
+
# @return [Array<PromptNote>]
|
|
55
|
+
def prompt_notes
|
|
56
|
+
return model_notes.map { |note| PromptNote.from_note(note) } unless @session
|
|
57
|
+
|
|
58
|
+
saved = @session.prompt_notes
|
|
59
|
+
return saved.dup if !@prompt_builder.prompt_notes.nil? || !saved.empty? || @session.messages.to_a.any?
|
|
60
|
+
|
|
61
|
+
model_notes.map { |note| PromptNote.from_note(note) }
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
Log.warn(:model, "prompt_notes_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
64
|
+
[]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Records +notes+ (ModelNotes::Note) on the session as the ones its
|
|
68
|
+
# prompt carries; saved with it.
|
|
69
|
+
def record_prompt_notes(notes)
|
|
70
|
+
@session.prompt_notes = Array(notes).map { |note| PromptNote.from_note(note) } if @session && notes
|
|
71
|
+
end
|
|
72
|
+
private :record_prompt_notes
|
|
73
|
+
|
|
74
|
+
# A failed turn's one line (the error's #summary). A host's failure
|
|
75
|
+
# under an alias says what the alias became (HostRegistry#alias_note):
|
|
76
|
+
# "host main" alone doesn't tell --model splash's user it was splash.
|
|
77
|
+
# @return [String]
|
|
78
|
+
def failure_summary(error)
|
|
79
|
+
summary = error.respond_to?(:summary) ? error.summary : error.message
|
|
80
|
+
return summary unless error.is_a?(LLM::ProviderError) && error.host
|
|
81
|
+
|
|
82
|
+
note = @host_registry.alias_note(effective_model_name)
|
|
83
|
+
note && note.end_with?("on host #{error.host}") ? "#{summary} (#{note})" : summary
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
EffectiveModel::Unresolved.fallback(:alias_note_failed, effective_model_name, e, summary)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# +name+'s resolved ref (ModelRef#ref).
|
|
89
|
+
def model_ref_for(name)
|
|
90
|
+
@host_registry.model_ref(name).ref
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The ref #switch_model! would switch +model_name+ to, checked the way it
|
|
94
|
+
# checks one (UnknownHost, MissingModel), without switching.
|
|
95
|
+
# @return [String]
|
|
96
|
+
def model_switch_ref(model_name)
|
|
97
|
+
model_ref_for(ModelProfile.check_host!(ModelProfile.required_model_name(model_name), hosts: @host_registry.entries))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Writes the effective model to +session+: the resolved ref, and the
|
|
101
|
+
# name as typed when that differs (model_typed: an alias, for the
|
|
102
|
+
# models: lookup after a resume).
|
|
103
|
+
def store_model!(session)
|
|
104
|
+
ref = effective_model_ref
|
|
105
|
+
session.model_name = ref
|
|
106
|
+
typed = typed_model_name
|
|
107
|
+
session.model_typed = typed.to_s == ref ? nil : typed
|
|
108
|
+
session
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The effective model's key (ModelOverlay.key_for its bare name): memory
|
|
112
|
+
# overlays, guardrail rules' models:.
|
|
113
|
+
# @return [String, nil]
|
|
114
|
+
def model_key = @model_binding.model_key
|
|
115
|
+
|
|
116
|
+
# The prompt profile for the effective model (see #profile_resolution).
|
|
117
|
+
# @return [ModelProfile]
|
|
118
|
+
def profile = profile_resolution.profile
|
|
119
|
+
|
|
120
|
+
# Which profile the effective model gets and where that came from,
|
|
121
|
+
# kept with the model choice it was resolved for
|
|
122
|
+
# (ModelBinding#profile_resolution).
|
|
123
|
+
# @return [ModelProfile::Resolution]
|
|
124
|
+
def profile_resolution = @model_binding.profile_resolution
|
|
125
|
+
|
|
126
|
+
# The model id sent for +full_ref+ (its alias applied, the host prefix off).
|
|
127
|
+
def bare_model_name(full_ref)
|
|
128
|
+
@host_registry.bare_name(full_ref)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The memory overlay keys follow the model sent (ModelBinding#sync_model_key!).
|
|
132
|
+
def sync_model_key! = @model_binding.sync_model_key!
|
|
133
|
+
private :sync_model_key!
|
|
134
|
+
|
|
135
|
+
# The effective model's bare name for the guardrails' models: rules;
|
|
136
|
+
# nil without a model name.
|
|
137
|
+
def guardrail_model_name
|
|
138
|
+
model = effective_model_name
|
|
139
|
+
name = model && bare_model_name(model)
|
|
140
|
+
name.to_s.strip.empty? ? nil : name
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Point the kernel (and a chat backend) at the effective model's host
|
|
144
|
+
# (after /model, --model, resume).
|
|
145
|
+
def sync_kernel_client!(model = effective_model)
|
|
146
|
+
@client = model.client
|
|
147
|
+
@kernel.client = model.client if @kernel.client != model.client
|
|
148
|
+
backend_for(model)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# ── Model switching ────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
# @param typed [String, nil] the name the model was typed as when
|
|
154
|
+
# +model_name+ is a stored ref (a resumed session's model_typed)
|
|
155
|
+
# @return [String] the resolved ref the session stores (ModelRef#ref)
|
|
156
|
+
def switch_model!(model_name, persist_default: false, typed: nil)
|
|
157
|
+
# The alias is applied where the model is resolved (HostRegistry#resolve),
|
|
158
|
+
# once; checked here: an unknown host, an alias naming another host.
|
|
159
|
+
resolved = ModelProfile.check_host!(ModelProfile.required_model_name(model_name), hosts: @host_registry.entries)
|
|
160
|
+
choice = ModelChoice.new(name: resolved, typed: typed.to_s.strip.empty? ? model_name : typed.to_s.strip)
|
|
161
|
+
@model_binding.switch!(choice)
|
|
162
|
+
sync_model_key!
|
|
163
|
+
@prompt_builder.reset!
|
|
164
|
+
# The notes the new model's prompt loads at its next build, so /model,
|
|
165
|
+
# /stats and the web name them before the next turn does.
|
|
166
|
+
record_prompt_notes(model_notes) if @session
|
|
167
|
+
sync_kernel_client!
|
|
168
|
+
@client.invalidate_context_window!
|
|
169
|
+
@metrics.forget_model_reports!
|
|
170
|
+
# The last turn's ctx % was the old model's window's
|
|
171
|
+
# (#announce_switched_context! sends the new one's).
|
|
172
|
+
@last_context_status = nil
|
|
173
|
+
ref = effective_model_ref
|
|
174
|
+
if persist_default
|
|
175
|
+
ConfigFile.write_default_model!(ref)
|
|
176
|
+
@default_model_name = ref
|
|
177
|
+
end
|
|
178
|
+
ref
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# After /model: how full the context is on the new model's window,
|
|
182
|
+
# before its first turn: the last turn's count (the old model's
|
|
183
|
+
# tokenizer's, replaced at the next generation) against the new model's
|
|
184
|
+
# window (EffectiveModel#context_window, ContextWindow.resolve: the
|
|
185
|
+
# running server first, one /props GET that a remote chat host is
|
|
186
|
+
# spared; else the host's cached model list; else the configured
|
|
187
|
+
# window) and the
|
|
188
|
+
# session's budget. Sent as a :context_status event (the web's ctx
|
|
189
|
+
# meter, the attached TUI's status row) with context_window_tokens, and
|
|
190
|
+
# kept for a client that joins later (#session_state_snapshot; the
|
|
191
|
+
# window in the metrics' context too, which a page counts from). A window
|
|
192
|
+
# that can't be resolved sends usage nil: the UIs clear the % until the
|
|
193
|
+
# next generation. Nothing before the session's first counted turn.
|
|
194
|
+
# +configured_only+: the configured window only, never asking a server
|
|
195
|
+
# (EffectiveModel#configured_context_window): a loaded session's (#session=).
|
|
196
|
+
# @return [Hash, nil] the event sent
|
|
197
|
+
def announce_switched_context!(configured_only: false)
|
|
198
|
+
used = @metrics.snapshot.dig(:context, :used_tokens)
|
|
199
|
+
return nil unless used.is_a?(Integer)
|
|
200
|
+
|
|
201
|
+
model = effective_model
|
|
202
|
+
window = configured_only ? model.configured_context_window : current_context_window(model)
|
|
203
|
+
display = window && saved_context_status({ used_tokens: used, window_tokens: window.tokens })
|
|
204
|
+
@last_context_status = display&.dup
|
|
205
|
+
@metrics.note_context_window(window.tokens, window.source) if window
|
|
206
|
+
usage = display && { estimated_pct: display[:est_pct], estimated_used_tokens: used, window_tokens: window.tokens,
|
|
207
|
+
window_source: window.source.to_s, source: "switch" }
|
|
208
|
+
event = { type: :context_status, usage: usage, bucket: display&.dig(:bucket),
|
|
209
|
+
context_window_tokens: window&.tokens, context_window_source: window&.source&.to_s }
|
|
210
|
+
@session_observer.notify(event)
|
|
211
|
+
event
|
|
212
|
+
rescue StandardError => e
|
|
213
|
+
Log.warn(:model, "switched_context_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
214
|
+
nil
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Whether the last turn that named its model ran on another one than
|
|
218
|
+
# the effective model; false before any (or when the ref can't be told).
|
|
219
|
+
def model_switched_since_last_turn?
|
|
220
|
+
last = @metrics.last_model_ref
|
|
221
|
+
!last.nil? && last != effective_model_ref
|
|
222
|
+
rescue StandardError => e
|
|
223
|
+
Log.warn(:model, "switch_check_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
224
|
+
false
|
|
225
|
+
end
|
|
226
|
+
private :model_switched_since_last_turn?
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
# Pick the loop for +model+ (EffectiveModel); the chat loop is pointed
|
|
231
|
+
# at its host's adapter (one per host, kept for its cached model list).
|
|
232
|
+
def backend_for(model)
|
|
233
|
+
return @native_backend unless model.chat?
|
|
234
|
+
|
|
235
|
+
@chat_backend_mutex.synchronize do
|
|
236
|
+
@chat_backend ||= LLM::ChatLoop.new(kernel: @kernel, stable_length: ->(text) { @prompt_builder.stable_length(text) })
|
|
237
|
+
@chat_backend.adapter = @host_registry.adapter_for(model.entry)
|
|
238
|
+
@chat_backend.session_id = session&.id
|
|
239
|
+
@chat_backend
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# The window as +model+'s loop would see it (EffectiveModel#context_window).
|
|
244
|
+
def current_context_window(model) = model.context_window
|
|
245
|
+
|
|
246
|
+
# The effective model now, resolved on each call, never kept
|
|
247
|
+
# (SessionModelSettings#effective_model). Raises as HostRegistry#resolve does.
|
|
248
|
+
# @return [EffectiveModel]
|
|
249
|
+
def effective_model(choice = @model_binding.choice) = @model_settings.effective_model(choice)
|
|
250
|
+
|
|
251
|
+
# ── Prompt profile ─────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
# The profile's resolve, keep, apply and retry are ModelBinding's
|
|
254
|
+
# (#build_model_binding); these forward to it.
|
|
255
|
+
def resolve_profile(choice) = @model_binding.resolve_profile(choice)
|
|
256
|
+
def store_profile(choice, resolution) = @model_binding.store_profile(choice, resolution)
|
|
257
|
+
def apply_profile(resolution) = @model_binding.apply_profile(resolution)
|
|
258
|
+
def refresh_profile! = @model_binding.refresh_profile!
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# The session's objectives: their view announced when it changed, Check
|
|
6
|
+
# this's side request, the model's proposals kept as cards until the turn
|
|
7
|
+
# is over, System 1's audit of them (ObjectivesAudit). Writes
|
|
8
|
+
# @objectives_announced, @objectives_audit; reads @objective_proposals
|
|
9
|
+
# under @objective_proposals_mutex, @session, @side_judgments.
|
|
10
|
+
module ObjectivesPart
|
|
11
|
+
# The session's objectives as the page draws them (Objectives.view), when
|
|
12
|
+
# that differs from the last announced: after an edit (its file written,
|
|
13
|
+
# pending), once a list note is in, at a turn's end (its moves) and after
|
|
14
|
+
# a rollback. A session with no list announces nothing. It only informs:
|
|
15
|
+
# a failure is logged, never the turn's.
|
|
16
|
+
def announce_objectives(session = @session)
|
|
17
|
+
return unless session
|
|
18
|
+
|
|
19
|
+
synchronize_events do
|
|
20
|
+
view = Objectives.view(session.id, session.messages, state_dir: session_state_dir, running: live_conversation&.dup)
|
|
21
|
+
next if view.nil? || view == @objectives_announced
|
|
22
|
+
|
|
23
|
+
@objectives_announced = view
|
|
24
|
+
announce({ type: :objectives_updated, session_id: session.id, **view })
|
|
25
|
+
end
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
Log.warn(:turn, "objectives_announce_failed", error: e.class.name, msg: e.message)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Check this (ObjectivesCheck)'s one side request. Its usage is in the
|
|
31
|
+
# session's side line. Raises as ctx.ask_model's request does.
|
|
32
|
+
# @param request [Array<Hash>] chat messages
|
|
33
|
+
# @param model [String, nil] a configured model; nil: the session's
|
|
34
|
+
# @return [String] the answer
|
|
35
|
+
def ask_objective_check(request, model:)
|
|
36
|
+
ask_side_model(request, timeout: ObjectivesCheck::TIMEOUT_SECONDS, max_tokens: ObjectivesCheck::MAX_TOKENS,
|
|
37
|
+
cancel_controller: nil, model: model, source: ObjectivesCheck::SOURCE)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A change the model proposes (the objectives tool), kept until the turn
|
|
41
|
+
# is over, as #context_choice's cards are: a card shown then is a quiet one, between turns, with its Apply kept
|
|
42
|
+
# over a restart (CardStore), not a running turn's that says it waits on the
|
|
43
|
+
# user. The same change twice in a turn is one card.
|
|
44
|
+
# @return [Boolean] false past Objectives::PROPOSALS_PER_TURN
|
|
45
|
+
def propose_objective(proposal)
|
|
46
|
+
@objective_proposals_mutex.synchronize do
|
|
47
|
+
next true if @objective_proposals.any? { |p| p.line == proposal.line }
|
|
48
|
+
next false if @objective_proposals.size >= Objectives::PROPOSALS_PER_TURN
|
|
49
|
+
|
|
50
|
+
@objective_proposals << proposal
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def show_objective_proposals
|
|
56
|
+
drain_objective_proposals.each do |proposal|
|
|
57
|
+
id = "objectives-#{SecureRandom.hex(4)}"
|
|
58
|
+
show_card(source: Objectives::SOURCE, id: id, title: proposal.title, body: proposal.body,
|
|
59
|
+
actions: [{ label: "Apply", command: proposal.action_line(id) },
|
|
60
|
+
{ label: "Dismiss", command: "/objectives --card #{id} dismiss" }])
|
|
61
|
+
end
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
Log.warn(:turn, "objectives_proposal_failed", error: e.class.name, msg: e.message)
|
|
64
|
+
end
|
|
65
|
+
private :show_objective_proposals
|
|
66
|
+
|
|
67
|
+
# A turn that did not complete (canceled, failed) takes its proposals
|
|
68
|
+
# off the buffer without showing them: their Apply card would sit after a
|
|
69
|
+
# turn whose `objectives propose` call is not in the history (a rollback
|
|
70
|
+
# after Ctrl-C drops it), and leaving them in the buffer would show them
|
|
71
|
+
# at the end of the next turn.
|
|
72
|
+
def discard_objective_proposals(turn)
|
|
73
|
+
dropped = drain_objective_proposals
|
|
74
|
+
Log.info(:turn, "objectives_proposals_dropped", count: dropped.size, outcome: turn.outcome.to_s) unless dropped.empty?
|
|
75
|
+
end
|
|
76
|
+
private :discard_objective_proposals
|
|
77
|
+
|
|
78
|
+
def drain_objective_proposals
|
|
79
|
+
@objective_proposals_mutex.synchronize { @objective_proposals.slice!(0..) }
|
|
80
|
+
end
|
|
81
|
+
private :drain_objective_proposals
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# System 1 on the objectives, in audit (ObjectivesAudit): the owner's turns
|
|
86
|
+
# only, and for a message a person's: a reminder's or a delegate's prompt isn't one.
|
|
87
|
+
def audit_objectives_incoming(turn)
|
|
88
|
+
return unless turn.prompt.is_a?(String) && turn.origin.human? && OwnerPrompt.turn?(session: turn.session, author: turn.origin.author)
|
|
89
|
+
|
|
90
|
+
objectives_audit.incoming(turn.session, turn.prompt)
|
|
91
|
+
end
|
|
92
|
+
private :audit_objectives_incoming
|
|
93
|
+
|
|
94
|
+
def audit_objectives_outgoing(turn, result)
|
|
95
|
+
return unless OwnerPrompt.turn?(session: turn.session, author: turn.origin.author)
|
|
96
|
+
|
|
97
|
+
objectives_audit.outgoing(turn.session, result.text)
|
|
98
|
+
end
|
|
99
|
+
private :audit_objectives_outgoing
|
|
100
|
+
|
|
101
|
+
# With saving off nothing would come of an ask, so none is made.
|
|
102
|
+
def objectives_audit
|
|
103
|
+
@objectives_audit ||= ObjectivesAudit.new(
|
|
104
|
+
side: @side_judgments, state_dir: -> { session_state_dir },
|
|
105
|
+
client_for: lambda do |judgment, session|
|
|
106
|
+
next nil unless system_one_runs.on?
|
|
107
|
+
|
|
108
|
+
system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: ObjectivesAudit::MODE, tag: :system_one),
|
|
109
|
+
judgment, session)
|
|
110
|
+
end
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
private :objectives_audit
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|