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,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# Cards and anytime commands: #show_card (routed by EventRoutes), the
|
|
6
|
+
# anytime and session-command thread marks, the anytime threads #shutdown
|
|
7
|
+
# waits for. Reads @event_routes, @anytime_threads under @lifecycle_mutex.
|
|
8
|
+
module CardsPart
|
|
9
|
+
# ── Cards ───────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
# Show a card in every UI (docs/plugins.md, Cards): {type: :card, id:,
|
|
12
|
+
# source:, title:, body:, level:, actions: [{label:, command:}], in_turn:}.
|
|
13
|
+
# Where it goes (held while the plugins load, an anytime command's, an
|
|
14
|
+
# init task's, a turn event, else announced) is EventRoutes#route's,
|
|
15
|
+
# marked in_turn: (CARD_MARKS). A card with the id of an earlier one
|
|
16
|
+
# replaces it (btw's "thinking…" → the answer).
|
|
17
|
+
# @param source [String] who shows it (a bundle's name)
|
|
18
|
+
# @param actions [Array<Hash>] {label:, command:}; a command is a line
|
|
19
|
+
# the session runs, as typed (D3)
|
|
20
|
+
# @return [String] the card's id
|
|
21
|
+
# @raise [ArgumentError] a card without a title, or a bad action
|
|
22
|
+
def show_card(source:, title:, body: "", actions: [], level: :info, id: nil)
|
|
23
|
+
card = Plugin::Card.build(source: source, title: title, body: body, actions: actions, level: level, id: id)
|
|
24
|
+
command = Thread.current[running_command_key]
|
|
25
|
+
card[:command] = command if command
|
|
26
|
+
@event_routes.route(card, **EventRoutes::CARD_MARKS)
|
|
27
|
+
card[:id]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Run an anytime command's block (D8): the cards and notices it shows on
|
|
31
|
+
# this thread are announced at once, marked anytime: true, and are
|
|
32
|
+
# never a running turn's events. They belong to the command, not the
|
|
33
|
+
# turn, and a UI shows them after the command's own line (a web
|
|
34
|
+
# command bubble drawn at its command_queued), even mid-turn.
|
|
35
|
+
# @return the block's value
|
|
36
|
+
def running_anytime
|
|
37
|
+
key = :"samagotchi_anytime_#{object_id}"
|
|
38
|
+
outer = Thread.current[key]
|
|
39
|
+
Thread.current[key] = true
|
|
40
|
+
yield
|
|
41
|
+
ensure
|
|
42
|
+
Thread.current[key] = outer
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Run a session command's dispatch: a card shown on this thread says
|
|
46
|
+
# it's that command's (:card's command:), for a reload that no longer has
|
|
47
|
+
# the command's line above it (Bridge::CardStore). A thread of its own
|
|
48
|
+
# isn't the command, nor a card shown after it.
|
|
49
|
+
# @param name [String] the command, "/short"
|
|
50
|
+
# @return the block's value
|
|
51
|
+
def running_command(name)
|
|
52
|
+
outer = Thread.current[running_command_key]
|
|
53
|
+
Thread.current[running_command_key] = name
|
|
54
|
+
yield
|
|
55
|
+
ensure
|
|
56
|
+
Thread.current[running_command_key] = outer
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Whether an anytime command (/btw) still runs on its thread.
|
|
60
|
+
def anytime_running?
|
|
61
|
+
@lifecycle_mutex.synchronize { @anytime_threads.any?(&:alive?) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Start an anytime command on its own thread (D8), one #shutdown waits
|
|
65
|
+
# for, so its command_ran is announced before the process leaves.
|
|
66
|
+
# @return [Thread]
|
|
67
|
+
def spawn_anytime(&)
|
|
68
|
+
thread = Thread.new(&)
|
|
69
|
+
@lifecycle_mutex.synchronize do
|
|
70
|
+
@anytime_threads.select!(&:alive?)
|
|
71
|
+
@anytime_threads << thread
|
|
72
|
+
end
|
|
73
|
+
thread
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def anytime_thread? = Thread.current[:"samagotchi_anytime_#{object_id}"] == true
|
|
77
|
+
private :anytime_thread?
|
|
78
|
+
|
|
79
|
+
def announce_anytime(event)
|
|
80
|
+
event = event.merge(anytime: true)
|
|
81
|
+
announce(event)
|
|
82
|
+
event
|
|
83
|
+
end
|
|
84
|
+
private :announce_anytime
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# The attached-context candidate judge (ContextJudge): its pass at an
|
|
6
|
+
# owner turn's start and for a late candidate, the asks it starts, and
|
|
7
|
+
# the cards it shows after the turn. Reads @context_cards_after_turn
|
|
8
|
+
# under @context_cards_mutex, @side_judgments, @host_registry, @session.
|
|
9
|
+
module ContextJudgePart
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# The candidate judge (ContextJudge) at an owner turn's start, after
|
|
13
|
+
# the toolkit's ask settled: the session's candidates' status changes
|
|
14
|
+
# are logged (ContextCandidatesLog#look) and, unless
|
|
15
|
+
# context.candidates.mode is off, the proposed candidates due an ask
|
|
16
|
+
# are asked about the owner's prompts so far, this one included. Never
|
|
17
|
+
# a participant's turn, a review session or a Continue (no new prompt).
|
|
18
|
+
def start_context_judge(turn)
|
|
19
|
+
session = turn.session
|
|
20
|
+
return unless turn.prompt.is_a?(String) && OwnerPrompt.turn?(session: session, author: turn.origin.author)
|
|
21
|
+
|
|
22
|
+
context_judge_pass(session, ContextCandidates.owner_texts(session.messages) + [turn.prompt], phase: "turn")
|
|
23
|
+
end
|
|
24
|
+
private :start_context_judge
|
|
25
|
+
|
|
26
|
+
# A late proposal no owner prompt mentions (Plugin::Host
|
|
27
|
+
# #candidate_proposed: github-pr's init task runs beside the first
|
|
28
|
+
# turn): judged on the owner's prompts so far, the running turn's too.
|
|
29
|
+
def judge_late_candidates
|
|
30
|
+
session = @session
|
|
31
|
+
return if session.nil? || session.review?
|
|
32
|
+
|
|
33
|
+
texts = ContextCandidates.owner_texts(plugin_messages)
|
|
34
|
+
context_judge_pass(session, texts, phase: "late") unless texts.empty?
|
|
35
|
+
end
|
|
36
|
+
private :judge_late_candidates
|
|
37
|
+
|
|
38
|
+
# One look at +session+'s candidates; the asks go to SideJudgments'
|
|
39
|
+
# :context kind, one thread asking each due candidate in turn.
|
|
40
|
+
def context_judge_pass(session, texts, phase:)
|
|
41
|
+
return if session.id.nil? || session.scratch
|
|
42
|
+
|
|
43
|
+
rows = ContextCandidates.with_status(session.id, project_root: session.project_root, state_dir: session_state_dir)
|
|
44
|
+
return if rows.empty?
|
|
45
|
+
|
|
46
|
+
log = ContextCandidatesLog.log_for(session_state_dir)
|
|
47
|
+
records = ContextCandidatesLog.records_for(log, session.id)
|
|
48
|
+
ContextCandidatesLog.look(log, session.id, rows, records) if phase == "turn"
|
|
49
|
+
settings = ContextJudge::Settings.from_config
|
|
50
|
+
return if settings.mode == "off"
|
|
51
|
+
|
|
52
|
+
jobs = ContextJudge.jobs(rows, records, session_id: session.id, texts: texts, settings: settings, phase: phase,
|
|
53
|
+
parent_attached: context_parent_attached(session))
|
|
54
|
+
return if jobs.empty?
|
|
55
|
+
|
|
56
|
+
client = system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :context),
|
|
57
|
+
ContextJudge::JUDGMENT, session, mode: settings.mode)
|
|
58
|
+
return unless client
|
|
59
|
+
|
|
60
|
+
start_context_asks(client, jobs, log, settings, context_choice(session, log, settings))
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
ContextJudge.warn(e)
|
|
63
|
+
end
|
|
64
|
+
private :context_judge_pass
|
|
65
|
+
|
|
66
|
+
# @return [Thread, nil] the asks' thread, nil when none started
|
|
67
|
+
def start_context_asks(client, jobs, log, settings, choice)
|
|
68
|
+
if @side_judgments.in_flight?(:context)
|
|
69
|
+
Log.info(:context, "context_judge_in_flight_skipped", session_id: jobs.first.session_id)
|
|
70
|
+
return nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@side_judgments.start(:context, warn: ContextJudge.method(:warn)) do
|
|
74
|
+
jobs.each { |job| choice.call(job, ContextJudge.ask(client: client, job: job, log: log, settings: settings)) }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
private :start_context_asks
|
|
78
|
+
|
|
79
|
+
# What an answer does on the asks' thread (ContextCandidateChoice
|
|
80
|
+
# #after_judged: on mode attaches or cards; audit nothing). Taken on
|
|
81
|
+
# the caller's thread. The attach only writes the session's folder: its
|
|
82
|
+
# note comes with the next between-turn delivery, never into a running
|
|
83
|
+
# turn. A card shows only while this Engine is still on that session,
|
|
84
|
+
# and not in a running turn (it isn't the turn's: no "waiting for you"
|
|
85
|
+
# on it), but once it has ended.
|
|
86
|
+
# @return [#call] (job, fields)
|
|
87
|
+
def context_choice(session, log, settings)
|
|
88
|
+
session_id = session.id
|
|
89
|
+
project_root = session.project_root
|
|
90
|
+
state_dir = session_state_dir
|
|
91
|
+
show = lambda do |**card|
|
|
92
|
+
current = @session
|
|
93
|
+
next nil unless current.nil? || current.id == session_id
|
|
94
|
+
|
|
95
|
+
@context_cards_mutex.synchronize do
|
|
96
|
+
next @context_cards_after_turn << card if turn_running?
|
|
97
|
+
|
|
98
|
+
show_card(**card)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
lambda do |job, fields|
|
|
102
|
+
ContextCandidateChoice.after_judged(job: job, fields: fields, settings: settings, project_root: project_root,
|
|
103
|
+
state_dir: state_dir, log: log, show_card: show)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
private :context_choice
|
|
107
|
+
|
|
108
|
+
# The cards #context_choice held while a turn ran (#run_turn's ensure,
|
|
109
|
+
# after the turn flag is released).
|
|
110
|
+
def show_context_cards_after_turn
|
|
111
|
+
cards = @context_cards_mutex.synchronize { @context_cards_after_turn.slice!(0..) }
|
|
112
|
+
cards.each { |card| show_card(**card) }
|
|
113
|
+
rescue StandardError => e
|
|
114
|
+
ContextJudge.warn(e)
|
|
115
|
+
end
|
|
116
|
+
private :show_context_cards_after_turn
|
|
117
|
+
|
|
118
|
+
# A delegate child's parent's sources, by title (else name): the
|
|
119
|
+
# judge's started_from. nil for any other session (a chi send --new
|
|
120
|
+
# session has no parent; a fork isn't a delegate).
|
|
121
|
+
def context_parent_attached(session)
|
|
122
|
+
return nil unless session.delegate? && session.parent_id
|
|
123
|
+
|
|
124
|
+
parent_root = if Session.exist?(session.parent_id, state_dir: session_state_dir)
|
|
125
|
+
Session.load(session.parent_id, state_dir: session_state_dir).project_root
|
|
126
|
+
else
|
|
127
|
+
session.project_root
|
|
128
|
+
end
|
|
129
|
+
ContextSources.attached(session.parent_id, project_root: parent_root, state_dir: session_state_dir)
|
|
130
|
+
.map { |attached| attached.source.title || attached.name }
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
Log.warn(:context, "context_parent_attached_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
133
|
+
[]
|
|
134
|
+
end
|
|
135
|
+
private :context_parent_attached
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# The event log: persistent observers, #announce (ANNOUNCEABLE_EVENTS),
|
|
6
|
+
# the event lock, held announcements, the load events, and the stream
|
|
7
|
+
# and emit helpers (EventFanout's, forwarded). Writes
|
|
8
|
+
# @guardrail_failures_announced; reads @session_observer, @event_fanout,
|
|
9
|
+
# @extension_load, @guardrail_failures, @plugin_failures.
|
|
10
|
+
module EventsPart
|
|
11
|
+
# Subscribe a persistent observer to engine events.
|
|
12
|
+
#
|
|
13
|
+
# Unlike the turn-scoped `on_event:` sink, a subscribed observer keeps
|
|
14
|
+
# receiving events across every `run_turn` call on this Engine. Each
|
|
15
|
+
# delivery carries a locally-monotonic `event_seq`. The returned handle can
|
|
16
|
+
# be used to unsubscribe later.
|
|
17
|
+
# @param observer [#call] receives event hashes (with `event_seq:` merged in)
|
|
18
|
+
# @return [Samagotchi::SessionObserver::SubscribedObserver] handle to unsubscribe
|
|
19
|
+
def subscribe(observer:)
|
|
20
|
+
@session_observer.subscribe(observer: observer)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Unsubscribe a previously-registered observer.
|
|
24
|
+
# @param handle [Samagotchi::SessionObserver::SubscribedObserver]
|
|
25
|
+
# @return [Boolean] whether the observer was removed (nil/unknown never raises)
|
|
26
|
+
def unsubscribe(handle:)
|
|
27
|
+
@session_observer.unsubscribe(handle: handle)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Integer] total engine events emitted so far (locally monotonic)
|
|
31
|
+
def event_count
|
|
32
|
+
@session_observer.event_count
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Put a transport-level event into the ordered event log. Unlike turn
|
|
36
|
+
# events it reaches only persistent observers (no turn sink, no memory
|
|
37
|
+
# capture).
|
|
38
|
+
# @param event [Hash] with :type in ANNOUNCEABLE_EVENTS
|
|
39
|
+
# @raise [ArgumentError] for any other type
|
|
40
|
+
def announce(event)
|
|
41
|
+
unless ANNOUNCEABLE_EVENTS.include?(event[:type])
|
|
42
|
+
raise ArgumentError, "cannot announce #{event[:type].inspect} (allowed: #{ANNOUNCEABLE_EVENTS.join(", ")})"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@session_observer.notify(event)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Run the block with the event log held: no event is numbered or
|
|
49
|
+
# delivered meanwhile, and events the block emits keep their order.
|
|
50
|
+
def synchronize_events(&)
|
|
51
|
+
@session_observer.synchronize(&)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The working set's :working_set_updated, announced between turns
|
|
55
|
+
# (after a command changed what the next request sends), when it
|
|
56
|
+
# differs from the last one emitted (#emit_working_set). Under the
|
|
57
|
+
# event lock, so it is ordered with the change's own events.
|
|
58
|
+
def announce_working_set(session = @session)
|
|
59
|
+
return unless session
|
|
60
|
+
|
|
61
|
+
synchronize_events { emit_working_set(nil, session, session.messages) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# ── Load events ───────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
# Announce what failed to load and what plugins showed while loading,
|
|
67
|
+
# once, as soon as the owner can show it (the worker's Bridge is up):
|
|
68
|
+
# the guardrail and plugin warnings, an orphaned
|
|
69
|
+
# project memory folder (label: "memory", MemoryOrphans), then the
|
|
70
|
+
# held notices (between_turns) and cards (in_turn: false, so late
|
|
71
|
+
# joiners get them). Without this call the first turn announces them
|
|
72
|
+
# (#announce_guardrail_failures).
|
|
73
|
+
def announce_load_events!
|
|
74
|
+
synchronize_events do
|
|
75
|
+
next if @guardrail_failures_announced
|
|
76
|
+
|
|
77
|
+
@guardrail_failures_announced = true
|
|
78
|
+
message = @guardrail_failures.message
|
|
79
|
+
announce({ type: :guardrail_warning, message: message }) if message
|
|
80
|
+
plugins = @plugin_failures.message
|
|
81
|
+
announce({ type: :guardrail_warning, message: plugins, label: "plugins" }) if plugins
|
|
82
|
+
memory = MemoryOrphans.message
|
|
83
|
+
announce({ type: :guardrail_warning, message: memory, label: "memory" }) if memory
|
|
84
|
+
@extension_load.held_events.each do |event|
|
|
85
|
+
announce(event[:type] == :card ? event.merge(in_turn: false) : event.merge(between_turns: true))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Run the block holding back the cards and between-turns notices it
|
|
92
|
+
# announces on this thread, so the caller announces them after its own
|
|
93
|
+
# event (a worker's command: its command_ran first, so a UI prints the
|
|
94
|
+
# command's output before its cards).
|
|
95
|
+
# @return [Array(Object, Array<Hash>)] the block's value and the held events
|
|
96
|
+
def holding_announcements
|
|
97
|
+
key = :"samagotchi_held_#{object_id}"
|
|
98
|
+
outer = Thread.current[key]
|
|
99
|
+
Thread.current[key] = []
|
|
100
|
+
value = yield
|
|
101
|
+
[value, Thread.current[key]]
|
|
102
|
+
ensure
|
|
103
|
+
Thread.current[key] = outer
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Announce +event+ now, or keep it for #holding_announcements' caller.
|
|
107
|
+
def announce_or_hold(event)
|
|
108
|
+
held = Thread.current[:"samagotchi_held_#{object_id}"]
|
|
109
|
+
held ? held << event : announce(event)
|
|
110
|
+
end
|
|
111
|
+
private :announce_or_hold
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# ── Event helpers ──────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
# The stream handler, the working set and the event fan-out are
|
|
118
|
+
# EventFanout's (#build_event_fanout); these forward to it.
|
|
119
|
+
def build_stream_event_handler(...) = @event_fanout.build_stream_event_handler(...)
|
|
120
|
+
def emit_working_set(...) = @event_fanout.emit_working_set(...)
|
|
121
|
+
private :emit_working_set
|
|
122
|
+
def working_set_llm_context = @event_fanout.working_set_llm_context
|
|
123
|
+
private :working_set_llm_context
|
|
124
|
+
def with_generation_report(event) = @event_fanout.with_generation_report(event)
|
|
125
|
+
private :with_generation_report
|
|
126
|
+
def stream_watch(cancel_controller) = @event_fanout.stream_watch(cancel_controller)
|
|
127
|
+
private :stream_watch
|
|
128
|
+
def emit_event(on_event, event) = @event_fanout.emit_event(on_event, event)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# The tool guardrails (GuardrailWiring, forwarded): who answers an
|
|
6
|
+
# approval, the rules and approvals, their state dir, and the load
|
|
7
|
+
# warnings the first turn announces. Writes @state_dir,
|
|
8
|
+
# @guardrail_failures_announced; reads @guardrail_wiring,
|
|
9
|
+
# @guardrail_failures, @plugin_failures, @hook_routes.
|
|
10
|
+
module GuardrailsPart
|
|
11
|
+
# ── Guardrails ─────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
# Who can answer an approval: :worker or :non_interactive (the
|
|
14
|
+
# default, so a bare Engine denies instead of waiting for nobody). Set
|
|
15
|
+
# by the host (TerminalUI, Worker).
|
|
16
|
+
def interface
|
|
17
|
+
@guardrail_wiring.interface
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def interface=(value)
|
|
21
|
+
@guardrail_wiring.interface = value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def guardrail_state_dir=(state_dir)
|
|
25
|
+
# Also where list_sessions and send_note look for other sessions.
|
|
26
|
+
@state_dir = state_dir
|
|
27
|
+
@guardrail_wiring.state_dir = state_dir
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The YAML rules, read again when a file changed (GuardrailWiring#rules).
|
|
31
|
+
# @return [Guardrails::Rules]
|
|
32
|
+
def guardrail_rules
|
|
33
|
+
@guardrail_wiring.rules
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Guardrails::LoadFailures]
|
|
37
|
+
attr_reader :guardrail_failures
|
|
38
|
+
|
|
39
|
+
# @return [Guardrails::Approvals]
|
|
40
|
+
def guardrail_approvals
|
|
41
|
+
@guardrail_wiring.approvals
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Guardrails::LoadFailures] the plugins that failed to load
|
|
45
|
+
attr_reader :plugin_failures
|
|
46
|
+
|
|
47
|
+
# Once per Engine, on its first turn: what failed to load, so every UI
|
|
48
|
+
# (the attached TUI, the web, a one-shot's stderr) shows it: the guardrails, then the plugins
|
|
49
|
+
# (label: "plugins"; the UIs say guardrails without one), an orphaned
|
|
50
|
+
# project memory folder (label: "memory"), then the notices and cards plugins showed as they loaded.
|
|
51
|
+
def announce_guardrail_failures(on_event)
|
|
52
|
+
return if @guardrail_failures_announced
|
|
53
|
+
|
|
54
|
+
@guardrail_failures_announced = true
|
|
55
|
+
message = @guardrail_failures.message
|
|
56
|
+
emit_event(on_event, { type: :guardrail_warning, message: message }) if message
|
|
57
|
+
plugins = @plugin_failures.message
|
|
58
|
+
emit_event(on_event, { type: :guardrail_warning, message: plugins, label: "plugins" }) if plugins
|
|
59
|
+
memory = MemoryOrphans.message
|
|
60
|
+
emit_event(on_event, { type: :guardrail_warning, message: memory, label: "memory" }) if memory
|
|
61
|
+
@extension_load.held_events.each { |event| emit_event(on_event, event) }
|
|
62
|
+
end
|
|
63
|
+
private :announce_guardrail_failures
|
|
64
|
+
|
|
65
|
+
# The warning the first turn announced (nil before it, or with nothing
|
|
66
|
+
# failed), for a UI that joins later (Bridge#snapshot).
|
|
67
|
+
def guardrail_warning
|
|
68
|
+
@guardrail_failures.message if @guardrail_failures_announced
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The plugins' load warning the first turn announced, likewise, and what
|
|
72
|
+
# their failed chi.on handlers told (HookRoutes#plugin_hook_failed).
|
|
73
|
+
def plugin_warning
|
|
74
|
+
loaded = @plugin_failures.message if @guardrail_failures_announced
|
|
75
|
+
[loaded, @hook_routes.hook_failures_message].compact.join("; ").then { |text| text unless text.empty? }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Ask the user to approve a call the gate voted `ask` on
|
|
79
|
+
# (GuardrailWiring#request_approval).
|
|
80
|
+
# @param verdict [Guardrails::Verdict]
|
|
81
|
+
# @return [Guardrails::Verdict]
|
|
82
|
+
def request_approval(verdict)
|
|
83
|
+
@guardrail_wiring.request_approval(verdict)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class Engine
|
|
5
|
+
# Hooks and reminders: turn-scoped hooks (register, clear), the hook
|
|
6
|
+
# runtime's notify/ask/stop (HookRoutes, forwarded), the copy of the
|
|
7
|
+
# conversation a hook reads, and the due reminders. Reads @hooks,
|
|
8
|
+
# @hook_routes, @reminder_queue, @reminder_store.
|
|
9
|
+
module HooksPart
|
|
10
|
+
# @return [Array<String>] reminder names queued for a reminder turn
|
|
11
|
+
def due_reminder_names
|
|
12
|
+
@reminder_queue.pending_names
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Queue reminder names for a reminder turn (IdleReminders callback).
|
|
16
|
+
def note_due_reminders(names)
|
|
17
|
+
@reminder_queue.note_pending(names)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def clear_due_reminder_names!
|
|
21
|
+
@reminder_queue.clear_pending!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# ── Hooks API ──────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
# Register a hook callback for a named lifecycle event.
|
|
27
|
+
#
|
|
28
|
+
# Hooks are turn-scoped: they are automatically cleared after each
|
|
29
|
+
# `run_turn` call so that a single turn's hooks do not leak into the next.
|
|
30
|
+
#
|
|
31
|
+
# A hook that raises tells the user once, as a bundle's does
|
|
32
|
+
# (HookRoutes#plugin_hook_failed); the registry logs each failure and goes on.
|
|
33
|
+
#
|
|
34
|
+
# @param name [Symbol] one of the hook names (see Hooks module)
|
|
35
|
+
# @param block [Proc] receives an event hash (may mutate in place)
|
|
36
|
+
# @return [void]
|
|
37
|
+
def register_hook(name, &block)
|
|
38
|
+
return @hooks.register(name) unless block
|
|
39
|
+
|
|
40
|
+
@hooks.register(name) do |event|
|
|
41
|
+
yield(event)
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
@hook_routes.plugin_hook_failed("#{Hooks::Registry::TURN_HOOK_LABEL} (#{name})", "#{e.class}: #{e.message}")
|
|
44
|
+
raise
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Unregister a previously registered hook.
|
|
49
|
+
# @param name [Symbol]
|
|
50
|
+
# @return [Boolean] true if it was removed, false if not found
|
|
51
|
+
def unregister_hook(name)
|
|
52
|
+
@hooks.unregister(name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Clear all registered hooks. Called automatically at the end of each
|
|
56
|
+
# `run_turn` to keep hooks turn-scoped.
|
|
57
|
+
# @return [void]
|
|
58
|
+
def clear_hooks
|
|
59
|
+
@hooks.clear_all
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# ── Reminders API ────────────────────────────────────────────────────────────
|
|
63
|
+
#
|
|
64
|
+
# Each turn injects the reminders due then (#turn_messages,
|
|
65
|
+
# ReminderQueue#inject!). Between turns the idle tick (IdleReminders)
|
|
66
|
+
# finds them due and its callback queues their names
|
|
67
|
+
# (#note_due_reminders); the worker runs a reminder turn
|
|
68
|
+
# (a continue turn) for them.
|
|
69
|
+
|
|
70
|
+
# @return [Boolean] whether any reminder is due now (the store's view,
|
|
71
|
+
# which a turn would inject), regardless of the queued names
|
|
72
|
+
def reminders_due?
|
|
73
|
+
@reminder_queue.due?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [ReminderStore] the reminder store for inspection
|
|
77
|
+
attr_reader :reminder_store
|
|
78
|
+
|
|
79
|
+
# The conversation as a hook may read it: a frozen array of copied
|
|
80
|
+
# messages, so a hook cannot change what the turn sends or stores.
|
|
81
|
+
def hook_messages(messages)
|
|
82
|
+
AnswerDisplay.strip_all(messages).map(&:dup).freeze
|
|
83
|
+
end
|
|
84
|
+
private :hook_messages
|
|
85
|
+
|
|
86
|
+
# ── The hook runtime (HookRoutes) ───────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
# What a hook (Hooks::Runtime) and a plugin (Plugin::Host) can do beyond
|
|
89
|
+
# reading its event, routed to the UIs by HookRoutes; these forward.
|
|
90
|
+
def hook_runtime = @hook_routes.runtime
|
|
91
|
+
private :hook_runtime
|
|
92
|
+
|
|
93
|
+
def hook_source(hook) = @hook_routes.source(hook)
|
|
94
|
+
private :hook_source
|
|
95
|
+
|
|
96
|
+
# One line to the user (:hook_notice), HookRoutes#notify.
|
|
97
|
+
def hook_notify(text, level, hook, fallback_for: nil, extra: {})
|
|
98
|
+
@hook_routes.notify(text, level, hook, fallback_for: fallback_for, extra: extra)
|
|
99
|
+
end
|
|
100
|
+
private :hook_notify
|
|
101
|
+
|
|
102
|
+
def guardrail_notify(message) = @hook_routes.guardrail_notify(message)
|
|
103
|
+
private :guardrail_notify
|
|
104
|
+
|
|
105
|
+
# @param question [HookQuestion]
|
|
106
|
+
# @return [Hash, nil] {selected:, freeform:, selected_indices:}
|
|
107
|
+
def hook_ask_user(question) = @hook_routes.ask_user(question)
|
|
108
|
+
private :hook_ask_user
|
|
109
|
+
|
|
110
|
+
# @return [Boolean] true when a running turn was cancelled now
|
|
111
|
+
def hook_stop_turn(reason, hook) = @hook_routes.stop_turn(reason, hook)
|
|
112
|
+
private :hook_stop_turn
|
|
113
|
+
|
|
114
|
+
# @return [Boolean] true when a streaming generation was cut now
|
|
115
|
+
def hook_stop_generation(reason, hook) = @hook_routes.stop_generation(reason, hook)
|
|
116
|
+
private :hook_stop_generation
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|