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,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
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# The session's participants: the running turn's author, a pair turn's
|
|
6
|
+
# stop check, who came or went, the system prompt's participants line and
|
|
7
|
+
# a participant's author fields. Writes @participant_stop,
|
|
8
|
+
# @participants_line; reads @participants, @participants_watch,
|
|
9
|
+
# @guardrail_wiring.
|
|
10
|
+
module ParticipantsPart
|
|
11
|
+
# The running turn's author when a participant wrote it, else nil
|
|
12
|
+
# (ctx.turn_author, the hooks' author).
|
|
13
|
+
# @return [String, nil]
|
|
14
|
+
def turn_author = turn_running? ? @guardrail_wiring.turn_author : nil
|
|
15
|
+
|
|
16
|
+
# Whether a participant's pair turn must stop, read before each of its
|
|
17
|
+
# generations (#guard_participant_spend): author ("p:<id>") → why (their
|
|
18
|
+
# link ended, or they passed their cap), nil while it may go on. The
|
|
19
|
+
# Worker sets it (Worker#participant_turn_stop); without one (a one-shot)
|
|
20
|
+
# nothing is checked mid-turn.
|
|
21
|
+
attr_writer :participant_stop
|
|
22
|
+
|
|
23
|
+
# Before each generation (KernelLoop#generation_guard): a pair turn whose
|
|
24
|
+
# participant's link ended, or whose spend (Engine#metrics: their
|
|
25
|
+
# finished turns, this one's finished generations, their side requests)
|
|
26
|
+
# passed their cap, stops on the plugins' stop path (cost-cap's,
|
|
27
|
+
# work-cap's), with its notice. A generation's own cost is known only
|
|
28
|
+
# once it ends, so a turn can pass the cap by its last generation. The
|
|
29
|
+
# turn-start check is the Worker's (Worker#participant_refusal); a
|
|
30
|
+
# review turn only reads, and keeps to it.
|
|
31
|
+
def guard_participant_spend
|
|
32
|
+
author = turn_author
|
|
33
|
+
return if author.nil? || @participant_stop.nil?
|
|
34
|
+
return unless @guardrail_wiring.turn_mode == ParticipantMode::PAIR
|
|
35
|
+
|
|
36
|
+
reason = @participant_stop.call(author) or return
|
|
37
|
+
hook_stop_turn(reason, PARTICIPANTS_HOOK)
|
|
38
|
+
end
|
|
39
|
+
private :guard_participant_spend
|
|
40
|
+
|
|
41
|
+
# A participant joined (invited) or left (revoked, expired) since the
|
|
42
|
+
# last look: :participants_changed to the observers (ParticipantsWatch;
|
|
43
|
+
# the worker looks on its idle tick). The first look announces nobody.
|
|
44
|
+
# @return [ParticipantsWatch::Change, nil]
|
|
45
|
+
def announce_participants
|
|
46
|
+
change = @participants_watch.changes or return nil
|
|
47
|
+
|
|
48
|
+
announce(change.to_event)
|
|
49
|
+
change
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Reads the session's participants at a turn's start; the first to join,
|
|
53
|
+
# the first invited to act (a pair participant: the line's words) and
|
|
54
|
+
# the last to leave rebuild the system prompt (its participants line),
|
|
55
|
+
# once each: a turn in between sends the same prefix.
|
|
56
|
+
def refresh_participants!(session)
|
|
57
|
+
line = @participants.refresh!(session).prompt_mode(session)
|
|
58
|
+
return if line == @participants_line
|
|
59
|
+
|
|
60
|
+
@participants_line = line
|
|
61
|
+
@prompt_builder.reset!
|
|
62
|
+
end
|
|
63
|
+
private :refresh_participants!
|
|
64
|
+
|
|
65
|
+
# The author fields a message +author+ wrote carries (Steer.author_fields:
|
|
66
|
+
# author:, and author_name: from participants.json), {} for the owner.
|
|
67
|
+
# The worker's drain names a participant's merged lines with it, and
|
|
68
|
+
# its running save a participant's prompt.
|
|
69
|
+
# @param author [String, nil] a turn's or a line's author (TurnOrigin#author)
|
|
70
|
+
# @param session [Session, nil] the session it is for (the turn's)
|
|
71
|
+
def author_fields(author, session: @session)
|
|
72
|
+
return {} unless Steer.participant_author?(author)
|
|
73
|
+
|
|
74
|
+
Steer.author_fields(author, @participants.name_for(author, session: session))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A participant's opening message keeps the rules it was delivered under
|
|
78
|
+
# (ParticipantMode), stamped before any request carries it, so its header
|
|
79
|
+
# never changes when the session's mode does.
|
|
80
|
+
def participant_mode_fields(author)
|
|
81
|
+
mode = Steer.participant_author?(author) && @guardrail_wiring.turn_mode
|
|
82
|
+
mode ? { participant_mode: mode.to_s } : {}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# The Engine as the plugins' host: loading them (Plugin::Registries), their
|
|
6
|
+
# init tasks and staged tool sets (PluginTasks), Plugin::Host's view of
|
|
7
|
+
# the session, ctx.messages and ctx.http's cancel. Writes nothing of its
|
|
8
|
+
# own; reads @plugin_tasks, @extension_load, @hook_routes, @tools,
|
|
9
|
+
# @tool_index, @toolkit_binding, @prompt_builder, @session.
|
|
10
|
+
module PluginsPart
|
|
11
|
+
# Add a plugin's init task (Plugin::Api#init at commit); it starts with
|
|
12
|
+
# #start_init_tasks!.
|
|
13
|
+
def add_init_task(bundle:, label:, plugin_label:, provides_tools:, quiet:, timeout:, failed: nil, &)
|
|
14
|
+
@plugin_tasks.add(bundle: bundle, label: label, plugin_label: plugin_label, provides_tools: provides_tools,
|
|
15
|
+
quiet: quiet, timeout: timeout, failed: failed, &)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Start the init tasks not started yet, each on its own thread
|
|
19
|
+
# (PluginTasks#start!).
|
|
20
|
+
def start_init_tasks!
|
|
21
|
+
@plugin_tasks.start!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The running init tasks a UI shows (not the quiet ones), for a UI that
|
|
25
|
+
# joins while they run (Bridge#snapshot, with the event log held).
|
|
26
|
+
# @return [Array<Hash>] {bundle:, id:, label:}
|
|
27
|
+
def init_tasks
|
|
28
|
+
@plugin_tasks.running
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Wait for the running init tasks that provide tools (PluginTasks#await).
|
|
32
|
+
# @return [Boolean] whether it waited
|
|
33
|
+
def await_init_tasks(controller = nil, on_event = nil)
|
|
34
|
+
@plugin_tasks.await(controller, on_event)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The init task this thread runs, or nil.
|
|
38
|
+
def current_init_task = @plugin_tasks.current
|
|
39
|
+
private :current_init_task
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def load_plugins
|
|
44
|
+
host = plugin_host
|
|
45
|
+
registries = Plugin::Registries.new(
|
|
46
|
+
commands: @command_registry, tools: @tools, hooks: @hooks,
|
|
47
|
+
context_for: lambda { |bundle, settings, label|
|
|
48
|
+
Plugin::Context.new(bundle: bundle, label: label, settings: settings, host: host)
|
|
49
|
+
},
|
|
50
|
+
tools_changed: -> { tools_changed! },
|
|
51
|
+
services: @services,
|
|
52
|
+
stage_tools: ->(bundle, specs, context) { stage_tools(bundle, specs, context) },
|
|
53
|
+
init: lambda { |bundle, label, plugin_label, provides_tools:, quiet:, timeout:, failed: nil, &block|
|
|
54
|
+
add_init_task(bundle: bundle, label: label, plugin_label: plugin_label, provides_tools: provides_tools,
|
|
55
|
+
quiet: quiet, timeout: timeout, failed: failed, &block)
|
|
56
|
+
},
|
|
57
|
+
toolkits: @toolkit_binding.plugin_kits,
|
|
58
|
+
tool_index: @tool_index,
|
|
59
|
+
peer_sources: @peer_sources,
|
|
60
|
+
prompt_sections: @prompt_sections,
|
|
61
|
+
prompt_changed: -> { prompt_changed! },
|
|
62
|
+
judgments: @judgments,
|
|
63
|
+
hook_failed: ->(hook, failed) { @hook_routes.plugin_hook_failed(hook, failed) }
|
|
64
|
+
)
|
|
65
|
+
# What plugins show while they load (an MCP server that didn't
|
|
66
|
+
# start) waits for the first turn, beside the load warnings: no UI
|
|
67
|
+
# is there yet, and the Engine isn't built.
|
|
68
|
+
@extension_load.load_plugins(registries, failures: @plugin_failures) do
|
|
69
|
+
warn_undeclared_judgments
|
|
70
|
+
warn_bundle_settings
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# config.yml bundles: names and keys that do nothing (BundleSettingsCheck),
|
|
75
|
+
# held like the judgments' line, for the same reason. Logged by every
|
|
76
|
+
# Engine; shown by the first to find them (BundleSettingsCheck.unshown).
|
|
77
|
+
def warn_bundle_settings
|
|
78
|
+
lines = BundleSettingsCheck.warnings(@extension_load.bundle_settings)
|
|
79
|
+
lines.each { |line| Log.warn(:config, "bundle_setting_unknown", msg: line) }
|
|
80
|
+
BundleSettingsCheck.unshown(lines).each { |line| hook_notify(line, :warn, "config") }
|
|
81
|
+
end
|
|
82
|
+
private :warn_bundle_settings
|
|
83
|
+
|
|
84
|
+
# Held with the plugins' own notices, so every UI gets it: a worker's
|
|
85
|
+
# stderr goes nowhere.
|
|
86
|
+
def warn_undeclared_judgments
|
|
87
|
+
@judgments.undeclared.each do |name|
|
|
88
|
+
Log.warn(:config, "judgment_undeclared", judgment: name)
|
|
89
|
+
hook_notify("system_one.judgments.#{name}: no loaded bundle declares it, so it does nothing", :warn, "config")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
private :warn_undeclared_judgments
|
|
93
|
+
|
|
94
|
+
# Keep a plugin's new tool set (chi.replace_tools, from any thread)
|
|
95
|
+
# for the turn thread (PluginTasks#stage_tools).
|
|
96
|
+
def stage_tools(bundle, specs, context)
|
|
97
|
+
@plugin_tasks.stage_tools(bundle, specs, context)
|
|
98
|
+
end
|
|
99
|
+
private :stage_tools
|
|
100
|
+
|
|
101
|
+
# Apply the staged tool sets, on the turn thread, before the turn's
|
|
102
|
+
# system prompt is built (PluginTasks#apply_staged_tools!). A changed
|
|
103
|
+
# set may change the chi group the search tool's description counts
|
|
104
|
+
# (a plugin tool the toolkit leaves out).
|
|
105
|
+
# @return [Boolean] whether the tools changed
|
|
106
|
+
def apply_staged_tools!
|
|
107
|
+
changed = @plugin_tasks.apply_staged_tools!
|
|
108
|
+
@tool_index.refresh! if changed
|
|
109
|
+
changed
|
|
110
|
+
end
|
|
111
|
+
public :apply_staged_tools!
|
|
112
|
+
|
|
113
|
+
# The tools changed (a plugin's chi.tools_changed!): the system prompts,
|
|
114
|
+
# which declare them, are built again on the next turn.
|
|
115
|
+
def tools_changed!
|
|
116
|
+
prompt_changed!
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# A plugin's prompt section would give other text now
|
|
120
|
+
# (chi.prompt_changed!): the system prompts are built again at their
|
|
121
|
+
# next build (the next turn's start; a running turn keeps the prompt it
|
|
122
|
+
# started with). One cache restart: the plugin's command asked first
|
|
123
|
+
# (Plugin::Context#cache_confirm).
|
|
124
|
+
def prompt_changed!
|
|
125
|
+
@prompt_builder&.reset!
|
|
126
|
+
end
|
|
127
|
+
public :prompt_changed!
|
|
128
|
+
|
|
129
|
+
# What a Plugin::Context reads and calls: the session now, and the
|
|
130
|
+
# hook runtime's notify and ask_user.
|
|
131
|
+
def plugin_host
|
|
132
|
+
Plugin::Host.new(
|
|
133
|
+
session_id: -> { @session&.id },
|
|
134
|
+
cwd: -> { @session&.working_directory },
|
|
135
|
+
messages: -> { plugin_messages },
|
|
136
|
+
messages_partial: -> { turn_running? && @running_turn_messages.nil? },
|
|
137
|
+
turn_running: -> { turn_running? },
|
|
138
|
+
notify: ->(text, level, label, fallback_for: nil) { hook_notify(text, level, label, fallback_for: fallback_for) },
|
|
139
|
+
ask_user: ->(question) { hook_ask_user(question) },
|
|
140
|
+
# An init task's is its own (a Ctrl-C ends a turn, not the task).
|
|
141
|
+
cancelled: lambda {
|
|
142
|
+
(task = current_init_task) ? task.cancelled? : active_cancel_controller&.cancelled?
|
|
143
|
+
},
|
|
144
|
+
card: ->(**card) { show_card(**card) },
|
|
145
|
+
steer: ->(text, label) { steer(text, source: hook_source(label)) },
|
|
146
|
+
stop_turn: ->(reason, label) { hook_stop_turn(reason, label) },
|
|
147
|
+
stop_generation: ->(reason, label) { hook_stop_generation(reason, label) },
|
|
148
|
+
toolkit: -> { @toolkit_binding.name },
|
|
149
|
+
declared_tools: -> { toolkit_explained.declared.map(&:name) },
|
|
150
|
+
use_toolkit: ->(name) { use_toolkit(name) },
|
|
151
|
+
load_tools: ->(names, bundle) { load_tools(names, bundle) },
|
|
152
|
+
cancel_controller: ->(handler: false) { plugin_cancel_controller(handler: handler) },
|
|
153
|
+
ask_model: lambda { |request, timeout:, max_tokens:, cancel_controller:, model: nil, source: nil|
|
|
154
|
+
ask_side_model(request, timeout: timeout, max_tokens: max_tokens, cancel_controller: cancel_controller,
|
|
155
|
+
model: model, source: source)
|
|
156
|
+
},
|
|
157
|
+
model_name: -> { effective_model_ref },
|
|
158
|
+
model_key: -> { @model_binding.model_key },
|
|
159
|
+
# The names a models: map looks the model up under; [] when it doesn't resolve.
|
|
160
|
+
model_names: lambda do
|
|
161
|
+
effective_model.names
|
|
162
|
+
rescue StandardError
|
|
163
|
+
[]
|
|
164
|
+
end,
|
|
165
|
+
state_dir: -> { session_state_dir },
|
|
166
|
+
scratch: -> { @scratch },
|
|
167
|
+
setup: -> { SessionSetup.of(@session) },
|
|
168
|
+
# The files whose text the built system prompt holds; [] until a
|
|
169
|
+
# build and after a rebuild that drops them (SystemPrompt#prompt_files
|
|
170
|
+
# returns [] itself: the || keeps a nil builder safe).
|
|
171
|
+
prompt_files: -> { @prompt_builder&.prompt_files || [] },
|
|
172
|
+
frontend: -> { interface },
|
|
173
|
+
cost: -> { @metrics.cost },
|
|
174
|
+
usage: -> { @metrics.usage },
|
|
175
|
+
turn_author: -> { turn_author },
|
|
176
|
+
# A plugin's prompt section is in the stable part: a full re-read.
|
|
177
|
+
cache_confirm: ->(line, changed) { CacheConfirm.ask(line: line, cost: :full, changed: changed, engine: self) },
|
|
178
|
+
candidate_proposed: ->(_candidate) { judge_late_candidates },
|
|
179
|
+
judgments: -> { @judgments }
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# The controller a plugin's ctx.http call without cancel: stops with
|
|
184
|
+
# (docs/plugins.md): an init task's own, else the running turn's for the
|
|
185
|
+
# turn's own work until the turn has ended (TurnState#own_controller); nil
|
|
186
|
+
# for an anytime command, a service or a thread of the plugin's.
|
|
187
|
+
def plugin_cancel_controller(handler:)
|
|
188
|
+
(task = current_init_task) ? task.cancel : @turn_state.own_controller(handler: handler)
|
|
189
|
+
end
|
|
190
|
+
private :plugin_cancel_controller
|
|
191
|
+
|
|
192
|
+
# A plugin's ctx.messages: the conversation without the system prompt
|
|
193
|
+
# (a session saved by an older chi's REPL may start with it), and,
|
|
194
|
+
# while a turn runs, the turn so far from the Bridge (plan O1). Read
|
|
195
|
+
# with the event log held, so a turn is in exactly one of the two.
|
|
196
|
+
def plugin_messages
|
|
197
|
+
synchronize_events do
|
|
198
|
+
messages = AnswerDisplay.strip_all(messages_checkpoint)
|
|
199
|
+
first = messages.first
|
|
200
|
+
messages = messages.drop(1) if first && first[:role].to_s == "system" && first[:kind].to_s.empty?
|
|
201
|
+
running = turn_running? && @running_turn_messages ? Array(@running_turn_messages.call) : []
|
|
202
|
+
messages + running
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
private :plugin_messages
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|