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,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
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# Ask-user-question: the model's question (its :question_asked hooks),
|
|
6
|
+
# and opening, posting, answering and cancelling questions
|
|
7
|
+
# (QuestionDesk, forwarded). Reads @question_desk, @hooks, @kernel,
|
|
8
|
+
# @session.
|
|
9
|
+
module QuestionsPart
|
|
10
|
+
# ── Ask-user-question (structured qualification) ──────────────────────────
|
|
11
|
+
|
|
12
|
+
# @return [PendingQuestion, nil] the question pending now
|
|
13
|
+
def pending_question
|
|
14
|
+
@question_desk.pending
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Ask the model's question (ask_user_question): the kernel's
|
|
18
|
+
# question_handler, called on the turn thread with the payload
|
|
19
|
+
# Tools::AskUserQuestion.validate made (QuestionDesk#request), after
|
|
20
|
+
# the :question_asked hooks (QuestionAsked) gave the step its refs; a
|
|
21
|
+
# --non-interactive run has no one to ask, so they don't fire there.
|
|
22
|
+
# @param payload [Hash] {question:, options:, header:, multi_select:, allow_freeform:}
|
|
23
|
+
# @return [String] normalized answer JSON
|
|
24
|
+
def request_question(payload)
|
|
25
|
+
payload = question_asked(QuestionAsked::KIND_QUESTION, payload) unless interface == :non_interactive
|
|
26
|
+
@question_desk.request(payload)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The :question_asked hooks for a question of the model's about to
|
|
30
|
+
# open: the fields with the asking step's refs (QuestionAsked.fire).
|
|
31
|
+
# @return [Hash]
|
|
32
|
+
def question_asked(kind, fields)
|
|
33
|
+
QuestionAsked.fire(hooks: @hooks, conversation: live_conversation, kind: kind, fields: fields,
|
|
34
|
+
cwd: @session&.working_directory, synchronize: ->(&block) { synchronize_events(&block) },
|
|
35
|
+
visible: ->(content) { question_step_text(content) })
|
|
36
|
+
end
|
|
37
|
+
private :question_asked
|
|
38
|
+
|
|
39
|
+
# A step's text as the asking slot shows it: the native loop's raw
|
|
40
|
+
# response without its thoughts and tool-call bodies (a chat step has
|
|
41
|
+
# neither), then the markup's tokens (OutputFormatter.strip_markup).
|
|
42
|
+
def question_step_text(content)
|
|
43
|
+
parser = @kernel.parser
|
|
44
|
+
OutputFormatter.strip_markup(parser.strip_tool_calls(parser.strip_thought(content)))
|
|
45
|
+
end
|
|
46
|
+
private :question_step_text
|
|
47
|
+
|
|
48
|
+
# Open a question for the UIs and wait for its answer
|
|
49
|
+
# (QuestionDesk#open_question: BLOCKS until answered or cancelled).
|
|
50
|
+
# @param fields [Hash] question:, options:, header:, multi_select:, allow_freeform:, …
|
|
51
|
+
# (as a Hash or as keywords)
|
|
52
|
+
# @param watch [#call, nil] closes the question with the reason it returns
|
|
53
|
+
# @return [QuestionAnswer, Hash, String] the answer, or {error:, …}, or a
|
|
54
|
+
# sync handler's text
|
|
55
|
+
def open_question(fields = nil, watch: nil, **more)
|
|
56
|
+
@question_desk.open_question({ **fields.to_h, **more }, watch: watch)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Publish a standing question that blocks nobody (QuestionDesk#post: a
|
|
60
|
+
# worker's step-limit question).
|
|
61
|
+
# @return [PendingQuestion, nil] the pending question, nil while another is open
|
|
62
|
+
def post_question(fields, on_answer:, on_superseded_close: nil)
|
|
63
|
+
@question_desk.post(fields, on_answer: on_answer, on_superseded_close: on_superseded_close)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Withdraw the standing question (QuestionDesk#withdraw).
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
def withdraw_question(reason, id: nil)
|
|
69
|
+
@question_desk.withdraw(reason, id: id)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Answer the pending question (called from UI thread; QuestionDesk#answer).
|
|
73
|
+
# @return [QuestionAnswer]
|
|
74
|
+
def answer_question(id:, selected:, freeform: nil, client_id: nil, parent_agent: nil)
|
|
75
|
+
@question_desk.answer(id: id, selected: selected, freeform: freeform, client_id: client_id,
|
|
76
|
+
parent_agent: parent_agent)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Mark the pending question as relayed, or clear the mark
|
|
80
|
+
# (QuestionDesk#annotate).
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
def annotate_question(id, relayed_to:, reason: nil)
|
|
83
|
+
@question_desk.annotate(id, relayed_to: relayed_to, reason: reason)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def set_question_sync_handler(&block)
|
|
87
|
+
@question_desk.sync_handler = block
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Cancel the pending question (QuestionDesk#cancel).
|
|
91
|
+
# @return [Boolean] whether it was cancelled
|
|
92
|
+
# @raise [QuestionDesk::NotDismissable] +id+ names a standing question
|
|
93
|
+
def cancel_question(reason = "user", id: nil)
|
|
94
|
+
@question_desk.cancel(reason, id: id)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# The idle recap (IdleRecap) and the shared idle scheduler: the recap job
|
|
6
|
+
# rebuilt under the bound session's own recap values, /recap's override,
|
|
7
|
+
# the saved recap. Writes @recap, @recap_awaiting_continue; reads
|
|
8
|
+
# @recap_built_for, @idle_scheduler, @session.
|
|
9
|
+
module RecapPart
|
|
10
|
+
# Snapshot the current session messages as a JSON string for the idle
|
|
11
|
+
# recap. The array is never mutated in place (a turn or a note replaces
|
|
12
|
+
# it), so the reference read here is a consistent snapshot; a dup'd
|
|
13
|
+
# copy is serialized. Never mutates session.messages.
|
|
14
|
+
# @return [String] JSON array of the messages
|
|
15
|
+
def messages_json_for_recap
|
|
16
|
+
snapshot = @session&.messages
|
|
17
|
+
return "[]" if snapshot.nil?
|
|
18
|
+
|
|
19
|
+
JSON.generate(AnswerDisplay.strip_all(snapshot).map(&:dup))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Emit a :recap_ready event (additive slot) carrying the generated recap
|
|
23
|
+
# and the generation id an observer uses to reject an invalidated recap.
|
|
24
|
+
# +covered+ counts the session messages it summarizes; +created_at+ is
|
|
25
|
+
# the saved recap's (UTC ISO; the UIs show its local time).
|
|
26
|
+
def emit_recap(recap:, generation:, covered: nil, created_at: nil)
|
|
27
|
+
event = { type: :recap_ready, recap: recap, generation: generation, covered: covered }
|
|
28
|
+
event[:created_at] = created_at if created_at
|
|
29
|
+
@session_observer.notify(event)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [IdleRecap, nil] the idle recap detector, or nil when disabled
|
|
33
|
+
attr_reader :recap
|
|
34
|
+
|
|
35
|
+
# Whether a continue offer waits (the Worker's or the one-shot's TurnFlow),
|
|
36
|
+
# for the recap job: this one and any rebuilt one (IdleRecap#awaiting_continue=).
|
|
37
|
+
def recap_awaiting_continue=(callable)
|
|
38
|
+
@recap_awaiting_continue = callable
|
|
39
|
+
@recap&.awaiting_continue = callable
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The bound session's own recap.* values ({} for none).
|
|
43
|
+
def session_recap_values
|
|
44
|
+
(@session&.settings&.core || {}).select { |key, _| key.start_with?("recap.") }
|
|
45
|
+
end
|
|
46
|
+
private :session_recap_values
|
|
47
|
+
|
|
48
|
+
# A new recap job when the bound session's own recap values differ from
|
|
49
|
+
# the ones the current job was built under (none at start): the old one
|
|
50
|
+
# is dropped (an attempt in flight goes stale) and the scheduler swaps
|
|
51
|
+
# it, keeping its one thread. Binding the same session again changes
|
|
52
|
+
# nothing.
|
|
53
|
+
def rebuild_recap!
|
|
54
|
+
return if session_recap_values == @recap_built_for
|
|
55
|
+
|
|
56
|
+
old = @recap
|
|
57
|
+
old&.invalidate!
|
|
58
|
+
@recap = build_recap
|
|
59
|
+
@idle_scheduler&.replace_job(old, @recap)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# /recap on|off|default: the bound session's own recap.enabled (true,
|
|
63
|
+
# false; nil removes it, so config.yml's applies again), and the recap
|
|
64
|
+
# job rebuilt under it at once. It doesn't save: the command says
|
|
65
|
+
# changed: [:recap] and its host saves (Worker#run_command).
|
|
66
|
+
# @param enabled [Boolean, nil]
|
|
67
|
+
# @raise [ArgumentError] no session is bound
|
|
68
|
+
def recap_override=(enabled)
|
|
69
|
+
raise ArgumentError, "no session" unless @session
|
|
70
|
+
|
|
71
|
+
core = @session.settings.core.dup
|
|
72
|
+
enabled.nil? ? core.delete(RECAP_ENABLED_KEY) : core[RECAP_ENABLED_KEY] = enabled
|
|
73
|
+
@session.settings = @session.settings.with(core: core)
|
|
74
|
+
rebuild_recap!
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The bound session's own recap.enabled (/recap on|off, --set), or nil.
|
|
78
|
+
# @return [Boolean, nil]
|
|
79
|
+
def recap_override = @session&.settings&.core&.fetch(RECAP_ENABLED_KEY, nil)
|
|
80
|
+
|
|
81
|
+
# Write the recap now, before the session is left (IdleRecap#write_now).
|
|
82
|
+
# @return [String, nil] the recap written, nil when none was (recap off,
|
|
83
|
+
# nothing new, an error, the timeout)
|
|
84
|
+
def write_recap_now(on_start: nil)
|
|
85
|
+
@recap&.write_now(on_start: on_start)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The longest #write_recap_now waits (recap.timeout), nil when no recap
|
|
89
|
+
# is set up.
|
|
90
|
+
# @return [Float, nil]
|
|
91
|
+
def recap_timeout = @recap&.timeout
|
|
92
|
+
|
|
93
|
+
# Ask for a recap now (/recap), without waiting for the idle window.
|
|
94
|
+
# @return [Symbol] IdleRecap#request_now's answer, or :off
|
|
95
|
+
def request_recap
|
|
96
|
+
@recap ? @recap.request_now : :off
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The recap saved with the current session (recap.json), and how many
|
|
100
|
+
# user turns came after it (0: it is current).
|
|
101
|
+
# @return [Hash, nil] {text:, covered:, turns_since:, created_at:}
|
|
102
|
+
def saved_recap
|
|
103
|
+
state = @recap&.state
|
|
104
|
+
return nil unless state
|
|
105
|
+
|
|
106
|
+
messages = @session&.messages || []
|
|
107
|
+
covered = state[:covered].to_i
|
|
108
|
+
turns_since = Array(messages).drop(covered).count { |m| Steer.turn_opening?(m) }
|
|
109
|
+
{ text: state[:text], covered: covered, turns_since: turns_since, created_at: state[:created_at] }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Start the shared idle scheduler (reminders + optional recap). The recap
|
|
113
|
+
# job is only registered when recap is configured; the reminders job is
|
|
114
|
+
# always present. SessionManager workers call it so reminders can
|
|
115
|
+
# trigger turns; a one-shot never starts it and polls nothing.
|
|
116
|
+
def start_idle
|
|
117
|
+
@idle_scheduler&.start
|
|
118
|
+
self
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Stop the shared idle scheduler (a worker as it leaves). +join+: wait for a tick under way (IdleScheduler#stop); a
|
|
122
|
+
# caller holding #synchronize_events passes false, since a recap's emit
|
|
123
|
+
# in that tick waits for the same lock.
|
|
124
|
+
def stop_idle(join: true)
|
|
125
|
+
@idle_scheduler&.stop(join: join)
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# The bound session and its prompt: #session= (its recap, metrics and
|
|
6
|
+
# memories), its state dir, the relay's view, and the system prompt with
|
|
7
|
+
# the memory names it holds. Writes @session, @session_state_dir,
|
|
8
|
+
# @running_turn_messages; reads @prompt_builder, @metrics, @used_memories,
|
|
9
|
+
# @question_desk, @state_dir.
|
|
10
|
+
module SessionPart
|
|
11
|
+
# @return [Array<String>] deduped used memory names (thread-safe copy)
|
|
12
|
+
def used_memory_names
|
|
13
|
+
@used_memories.names
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Array<String>] the memories hidden from this session (normalized names)
|
|
17
|
+
def muted_memory_names
|
|
18
|
+
@muted_memory_names.dup
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Array<String>] the names the session preloads (config baseline
|
|
22
|
+
# + --memory, minus mutes), known before the prompt is built, unlike
|
|
23
|
+
# #activated_memory_names
|
|
24
|
+
def preloaded_memory_names
|
|
25
|
+
@prompt_builder.preloaded_memory_names
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Where sessions live (a session's images/ are under it); a worker sets
|
|
29
|
+
# its own.
|
|
30
|
+
attr_writer :session_state_dir
|
|
31
|
+
|
|
32
|
+
# The metrics load the session's saved records from its state dir, once.
|
|
33
|
+
def bind_metrics(session)
|
|
34
|
+
return unless session&.id
|
|
35
|
+
|
|
36
|
+
@metrics.state_dir = session_state_dir
|
|
37
|
+
@metrics.session_id = session.id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def session_state_dir = @session_state_dir || Session.default_state_dir
|
|
41
|
+
|
|
42
|
+
# The running turn's metrics so far, saved as a turn a worker died in
|
|
43
|
+
# would leave them (SessionMetrics#persist's running_turn): a worker's
|
|
44
|
+
# turn checkpoint (TurnCheckpoint) calls it when it writes.
|
|
45
|
+
# @return [Boolean] whether they were saved
|
|
46
|
+
def checkpoint_metrics = @metrics.persist(state_dir: session_state_dir, running_turn: true)
|
|
47
|
+
|
|
48
|
+
# What a Bridge adds to a plugin's ctx.messages while a turn runs (the
|
|
49
|
+
# turn so far, as messages); nil without a Bridge (a one-shot).
|
|
50
|
+
attr_writer :running_turn_messages
|
|
51
|
+
|
|
52
|
+
# The system prompt for a target's loop (default: the effective model's):
|
|
53
|
+
# the chat loop's leaves out the raw-prompt tool text, since its tools go
|
|
54
|
+
# as schemas. Built once per loop (and again after a model switch) so the
|
|
55
|
+
# prompt prefix, and the server's KV cache for it, stay stable.
|
|
56
|
+
# @param model [EffectiveModel, nil]
|
|
57
|
+
# @return [String]
|
|
58
|
+
# The native prompt depends on the thinking level too (Gemma's token):
|
|
59
|
+
# a level change builds it again. A build that
|
|
60
|
+
# misses the cache names the model and the level through its own
|
|
61
|
+
# lambdas (#model_identity, #turn_thinking), which resolve again.
|
|
62
|
+
def system_prompt(model = nil)
|
|
63
|
+
model ||= effective_model
|
|
64
|
+
chat = model.chat?
|
|
65
|
+
level = chat ? nil : @model_settings.thinking_level(model)
|
|
66
|
+
prompt = @prompt_builder.build(chat: chat, thinking: level, layers: model.llm_context_layers(@model_settings.llm_context_session_args))
|
|
67
|
+
# The memory indexes this prompt holds: /stats, the snapshot, analytics.json.
|
|
68
|
+
@metrics.memory_index = @prompt_builder.memory_index
|
|
69
|
+
# The model notes it carries: the session records them (/stats, /model, the web).
|
|
70
|
+
record_prompt_notes(@prompt_builder.prompt_notes)
|
|
71
|
+
prompt
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The base prompt (specs, plugins' declarations).
|
|
75
|
+
def assist_system_prompt(chat: false)
|
|
76
|
+
@prompt_builder.base(chat: chat)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The --memory names activated while building the prompt; the TerminalUI
|
|
80
|
+
# mirrors them into its status line.
|
|
81
|
+
def activated_memory_names
|
|
82
|
+
@prompt_builder.activated_memory_names
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Session] current session (Engine owns create/resume)
|
|
86
|
+
attr_reader :session
|
|
87
|
+
|
|
88
|
+
# This worker's relays of its delegates' approvals (memory only).
|
|
89
|
+
# @return [RelayDesk]
|
|
90
|
+
attr_reader :relay_desk
|
|
91
|
+
|
|
92
|
+
# The relay's view of this Engine, or nil when it can't host a relay: a
|
|
93
|
+
# worker's Engine only (its Bridge is what a child verifies against;
|
|
94
|
+
# --non-interactive has no one to ask), never with a sync handler (no
|
|
95
|
+
# wait loop, no watch).
|
|
96
|
+
# @return [RelayPeer, nil]
|
|
97
|
+
def relay_peer
|
|
98
|
+
return nil unless interface == :worker && !@question_desk.sync_handler?
|
|
99
|
+
|
|
100
|
+
RelayPeer.new(self)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @return [String] the state dir holding this Engine's sessions
|
|
104
|
+
def peer_state_dir = @state_dir
|
|
105
|
+
|
|
106
|
+
# Set the current session outside a turn (the worker and the one-shot
|
|
107
|
+
# do, before the first turn, so the messages API and recap see it)
|
|
108
|
+
def session=(session)
|
|
109
|
+
@session = session
|
|
110
|
+
# One session per worker or one-shot process: its records carry this sid.
|
|
111
|
+
Log.session_id = session.id if session&.id
|
|
112
|
+
# Its own recap values (chi --set recap.enabled=false) rebuild the job.
|
|
113
|
+
rebuild_recap!
|
|
114
|
+
# A woken worker's /stats and status line count the turns before it.
|
|
115
|
+
bind_metrics(session)
|
|
116
|
+
@used_memories.absorb(session)
|
|
117
|
+
# Loaded on another model than its last turn ran on (a --resume
|
|
118
|
+
# one-shot's --model, a worker woken after a /model with no turn since):
|
|
119
|
+
# the saved ctx % was that model's window's. Drop it and count it as
|
|
120
|
+
# /model does, on a configured window only: this runs before a
|
|
121
|
+
# worker's Bridge serves, so no /props GET or model list here (a slow
|
|
122
|
+
# or hung host would hold up the worker's start). No configured
|
|
123
|
+
# window: no % (ctx ?) until the first generation reports one.
|
|
124
|
+
return unless model_switched_since_last_turn?
|
|
125
|
+
|
|
126
|
+
@metrics.forget_model_reports!
|
|
127
|
+
announce_switched_context!(configured_only: true)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|