samagotchi 0.154.0 → 0.155.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 +30 -1
- data/docs/architecture.md +3 -2
- data/docs/bundles.md +21 -0
- data/docs/cli.md +10 -8
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +4 -4
- data/docs/internals/attached-context.md +8 -6
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/bridge.md +18 -5
- data/docs/internals/engine.md +221 -100
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +12 -6
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +3 -3
- data/docs/testing.md +16 -1
- data/lib/samagotchi/answer_text.rb +1 -2
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bridge/card_places.rb +56 -7
- data/lib/samagotchi/bridge/card_store.rb +24 -9
- data/lib/samagotchi/bridge.rb +9 -0
- data/lib/samagotchi/bridge_client.rb +9 -5
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- 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/dev/manifest.yml +3 -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/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +1 -1
- data/lib/samagotchi/engine/context_judge.rb +19 -125
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/objectives.rb +22 -90
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +5 -2
- data/lib/samagotchi/engine/toolkit.rb +33 -190
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +162 -99
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/engine_snapshots.rb +9 -8
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +6 -4
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/idle_recap.rb +3 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +194 -216
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +66 -42
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/session_manager.rb +8 -13
- data/lib/samagotchi/session_retention.rb +24 -4
- data/lib/samagotchi/steer.rb +12 -1
- data/lib/samagotchi/stream_event.rb +22 -0
- data/lib/samagotchi/system_prompt.rb +84 -61
- data/lib/samagotchi/terminal_ui/attached_view.rb +0 -9
- data/lib/samagotchi/terminal_ui/input_support.rb +16 -15
- data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
- data/lib/samagotchi/terminal_ui/reline_seam.rb +0 -10
- data/lib/samagotchi/terminal_ui/status_row.rb +0 -7
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/tool_activity.rb +8 -6
- data/lib/samagotchi/tool_declarations.rb +4 -4
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +93 -101
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/registry.rb +43 -40
- data/lib/samagotchi/tools/tool_index.rb +135 -88
- data/lib/samagotchi/tools/tool_surface.rb +173 -0
- data/lib/samagotchi/turn_context.rb +67 -0
- data/lib/samagotchi/turn_flow.rb +7 -2
- data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
- data/lib/samagotchi/turn_stages/objectives.rb +146 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
- data/lib/samagotchi/turn_stages/warmup.rb +83 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -1
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +8 -1
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- 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/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/composer_mode.js +1 -1
- data/lib/samagotchi/web/public/format.js +3 -2
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +8 -4
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- data/lib/samagotchi/web/session_access_routes.rb +8 -0
- data/lib/samagotchi/web/session_show.rb +4 -0
- data/lib/samagotchi/worker.rb +8 -5
- data/lib/samagotchi.rb +0 -1
- metadata +19 -2
- data/lib/samagotchi/pending_input_queue.rb +0 -55
|
@@ -2,30 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Samagotchi
|
|
4
4
|
class Engine
|
|
5
|
-
# The session's objectives:
|
|
6
|
-
#
|
|
7
|
-
# is over, System 1's audit of them (ObjectivesAudit)
|
|
8
|
-
# @
|
|
9
|
-
#
|
|
5
|
+
# The session's objectives (TurnStages::Objectives, forwarded): the view
|
|
6
|
+
# announced when it changed, the model's proposals kept as cards until
|
|
7
|
+
# the turn is over, System 1's audit of them (ObjectivesAudit); and
|
|
8
|
+
# Check this's side request. Reads @objectives_stage, @side_judgments,
|
|
9
|
+
# @host_registry, @session.
|
|
10
10
|
module ObjectivesPart
|
|
11
|
-
# The session's objectives as the page draws them
|
|
12
|
-
#
|
|
13
|
-
|
|
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
|
|
11
|
+
# The session's objectives as the page draws them, when that differs
|
|
12
|
+
# from the last announced (TurnStages::Objectives#announce).
|
|
13
|
+
def announce_objectives(session = @session) = @objectives_stage.announce(session)
|
|
29
14
|
|
|
30
15
|
# Check this (ObjectivesCheck)'s one side request. Its usage is in the
|
|
31
16
|
# session's side line. Raises as ctx.ask_model's request does.
|
|
@@ -38,79 +23,26 @@ module Samagotchi
|
|
|
38
23
|
end
|
|
39
24
|
|
|
40
25
|
# A change the model proposes (the objectives tool), kept until the turn
|
|
41
|
-
# is over
|
|
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.
|
|
26
|
+
# is over (TurnStages::Objectives#propose).
|
|
44
27
|
# @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
|
|
28
|
+
def propose_objective(proposal) = @objectives_stage.propose(proposal)
|
|
82
29
|
|
|
83
30
|
private
|
|
84
31
|
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
32
|
+
# The objectives' stage (TurnStages::Objectives).
|
|
33
|
+
attr_reader :objectives_stage
|
|
34
|
+
|
|
35
|
+
# The stage over this Engine's side asks and its event lock (built in
|
|
36
|
+
# #initialize, once the host registry is).
|
|
37
|
+
def build_objectives_stage
|
|
38
|
+
TurnStages::Objectives.new(
|
|
39
|
+
side_judgments: @side_judgments, host_registry: @host_registry, state_dir: -> { session_state_dir },
|
|
40
|
+
recording: ->(client, judgment, session, mode:) { system_one_recording(client, judgment, session, mode: mode) },
|
|
41
|
+
runs: -> { system_one_runs }, synchronize: ->(&block) { synchronize_events(&block) },
|
|
42
|
+
announce: ->(event) { announce(event) }, running: -> { live_conversation },
|
|
43
|
+
show_card: ->(**card) { show_card(**card) }
|
|
111
44
|
)
|
|
112
45
|
end
|
|
113
|
-
private :objectives_audit
|
|
114
46
|
end
|
|
115
47
|
end
|
|
116
48
|
end
|
|
@@ -5,13 +5,16 @@ module Samagotchi
|
|
|
5
5
|
# The session's participants: the running turn's author, a pair turn's
|
|
6
6
|
# stop check, who came or went, the system prompt's participants line and
|
|
7
7
|
# a participant's author fields. Writes @participant_stop,
|
|
8
|
-
# @participants_line; reads @participants, @participants_watch
|
|
9
|
-
#
|
|
8
|
+
# @participants_line; reads @participants, @participants_watch and the
|
|
9
|
+
# turn's TurnContext.
|
|
10
10
|
module ParticipantsPart
|
|
11
11
|
# The running turn's author when a participant wrote it, else nil
|
|
12
|
-
# (ctx.turn_author, the hooks' author
|
|
12
|
+
# (ctx.turn_author, the hooks' author, the judgments' participant:,
|
|
13
|
+
# ObjectivesCheck): one read of the published TurnContext, so any
|
|
14
|
+
# thread sees the running turn's author from its first moment, never
|
|
15
|
+
# the last turn's.
|
|
13
16
|
# @return [String, nil]
|
|
14
|
-
def turn_author =
|
|
17
|
+
def turn_author = turn_context&.author
|
|
15
18
|
|
|
16
19
|
# Whether a participant's pair turn must stop, read before each of its
|
|
17
20
|
# generations (#guard_participant_spend): author ("p:<id>") → why (their
|
|
@@ -20,7 +23,7 @@ module Samagotchi
|
|
|
20
23
|
# nothing is checked mid-turn.
|
|
21
24
|
attr_writer :participant_stop
|
|
22
25
|
|
|
23
|
-
# Before each generation (KernelLoop#generation_guard): a pair turn whose
|
|
26
|
+
# Before each generation (KernelLoop::Services#generation_guard): a pair turn whose
|
|
24
27
|
# participant's link ended, or whose spend (Engine#metrics: their
|
|
25
28
|
# finished turns, this one's finished generations, their side requests)
|
|
26
29
|
# passed their cap, stops on the plugins' stop path (cost-cap's,
|
|
@@ -28,10 +31,11 @@ module Samagotchi
|
|
|
28
31
|
# once it ends, so a turn can pass the cap by its last generation. The
|
|
29
32
|
# turn-start check is the Worker's (Worker#participant_refusal); a
|
|
30
33
|
# review turn only reads, and keeps to it.
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
# @param context [TurnContext] the running turn's (the loop's)
|
|
35
|
+
def guard_participant_spend(context)
|
|
36
|
+
author = context&.author
|
|
33
37
|
return if author.nil? || @participant_stop.nil?
|
|
34
|
-
return unless
|
|
38
|
+
return unless context.participant_mode == ParticipantMode::PAIR
|
|
35
39
|
|
|
36
40
|
reason = @participant_stop.call(author) or return
|
|
37
41
|
hook_stop_turn(reason, PARTICIPANTS_HOOK)
|
|
@@ -77,8 +81,9 @@ module Samagotchi
|
|
|
77
81
|
# A participant's opening message keeps the rules it was delivered under
|
|
78
82
|
# (ParticipantMode), stamped before any request carries it, so its header
|
|
79
83
|
# never changes when the session's mode does.
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
# @param context [TurnContext] the turn's
|
|
85
|
+
def participant_mode_fields(context)
|
|
86
|
+
mode = context.participant? && context.participant_mode
|
|
82
87
|
mode ? { participant_mode: mode.to_s } : {}
|
|
83
88
|
end
|
|
84
89
|
end
|
|
@@ -101,11 +101,13 @@ module Samagotchi
|
|
|
101
101
|
# Apply the staged tool sets, on the turn thread, before the turn's
|
|
102
102
|
# system prompt is built (PluginTasks#apply_staged_tools!). A changed
|
|
103
103
|
# set may change the chi group the search tool's description counts
|
|
104
|
-
# (a plugin tool the toolkit leaves out).
|
|
104
|
+
# (a plugin tool the toolkit leaves out). Publishes the Registry it
|
|
105
|
+
# leaves (#publish_tools!).
|
|
105
106
|
# @return [Boolean] whether the tools changed
|
|
106
107
|
def apply_staged_tools!
|
|
107
108
|
changed = @plugin_tasks.apply_staged_tools!
|
|
108
109
|
@tool_index.refresh! if changed
|
|
110
|
+
publish_tools!
|
|
109
111
|
changed
|
|
110
112
|
end
|
|
111
113
|
public :apply_staged_tools!
|
|
@@ -15,7 +15,7 @@ module Samagotchi
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# Ask the model's question (ask_user_question): the kernel's
|
|
18
|
-
# question_handler, called on the turn thread with the payload
|
|
18
|
+
# Services#question_handler, called on the turn thread with the payload
|
|
19
19
|
# Tools::AskUserQuestion.validate made (QuestionDesk#request), after
|
|
20
20
|
# the :question_asked hooks (QuestionAsked) gave the step its refs; a
|
|
21
21
|
# --non-interactive run has no one to ask, so they don't fire there.
|
|
@@ -42,9 +42,12 @@ module Samagotchi
|
|
|
42
42
|
def side_author(kind)
|
|
43
43
|
# Check this is only ever the owner's, whichever turn runs as it ends.
|
|
44
44
|
return nil if kind.to_s == ObjectivesCheck::SOURCE
|
|
45
|
-
return nil unless turn_running? || kind.to_s == RECAP_SIDE_KIND
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
# One read each of the published contexts (TurnState): any thread.
|
|
47
|
+
running = turn_context
|
|
48
|
+
return running.author if running
|
|
49
|
+
|
|
50
|
+
kind.to_s == RECAP_SIDE_KIND ? last_turn_context&.author : nil
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
private
|
|
@@ -4,9 +4,9 @@ module Samagotchi
|
|
|
4
4
|
class Engine
|
|
5
5
|
# The session's toolkit: the kits and what a turn declares
|
|
6
6
|
# (ToolkitBinding, forwarded), ctx.load_tools and the staged loads a turn
|
|
7
|
-
# appends, and System 1's toolkit suggestion (
|
|
8
|
-
#
|
|
9
|
-
# @
|
|
7
|
+
# appends, and System 1's toolkit suggestion (TurnStages::ToolkitSuggestion,
|
|
8
|
+
# forwarded). Reads @toolkit_binding, @tools, @tool_index, @tool_loads,
|
|
9
|
+
# @toolkit_suggestion, @session.
|
|
10
10
|
module ToolkitPart
|
|
11
11
|
private
|
|
12
12
|
|
|
@@ -17,22 +17,29 @@ module Samagotchi
|
|
|
17
17
|
def toolkit_explained(toolkit = nil) = @toolkit_binding.explained(toolkit)
|
|
18
18
|
def toolkit_summary = @toolkit_binding.summary
|
|
19
19
|
|
|
20
|
+
# The Registry published for readers off the turn thread
|
|
21
|
+
# (ToolkitBinding#publish!): at the end of the Engine's build, after a
|
|
22
|
+
# turn's staged tool sets and its toolkit apply.
|
|
23
|
+
def publish_tools! = @toolkit_binding.publish!
|
|
24
|
+
|
|
20
25
|
# The tools this session registered (Tools::Registry), declared or not.
|
|
21
26
|
attr_reader :tools
|
|
22
27
|
|
|
23
28
|
public :tools, :toolkit_kits, :resolved_toolkit, :toolkit_explained, :toolkit_summary
|
|
24
29
|
|
|
25
30
|
# A plugin's ctx.load_tools: stage the reachable tools +names+ names
|
|
26
|
-
# (Tools::ToolIndex#deferred_name
|
|
27
|
-
# nor loaded with an earlier tool change, for
|
|
28
|
-
# that names no tool at all is logged; a
|
|
29
|
-
# skipped quietly.
|
|
31
|
+
# (Tools::ToolIndex#deferred_name, on the published Registry) that the
|
|
32
|
+
# session neither declares nor loaded with an earlier tool change, for
|
|
33
|
+
# bundle +bundle+. A name that names no tool at all is logged; a
|
|
34
|
+
# declared or removed one is skipped quietly.
|
|
30
35
|
# @return [Array<String>] the wire names staged
|
|
31
36
|
def load_tools(names, bundle)
|
|
32
37
|
added = ToolLoads.added(@session&.messages)
|
|
38
|
+
published = @toolkit_binding.published
|
|
39
|
+
surface = Tools::ToolSurface.for(published)
|
|
33
40
|
wanted = names.filter_map do |name|
|
|
34
|
-
wire = @tool_index.deferred_name(name)
|
|
35
|
-
Log.info(:plugins, "load_tools_skipped", bundle: bundle, tool: name) unless wire ||
|
|
41
|
+
wire = @tool_index.deferred_name(name, surface)
|
|
42
|
+
Log.info(:plugins, "load_tools_skipped", bundle: bundle, tool: name) unless wire || published.key?(name)
|
|
36
43
|
wire
|
|
37
44
|
end
|
|
38
45
|
@tool_loads.stage(wanted - added, by: bundle)
|
|
@@ -79,191 +86,27 @@ module Samagotchi
|
|
|
79
86
|
def apply_toolkit!(session, on_event = nil) = @toolkit_binding.apply!(session, on_event)
|
|
80
87
|
private :apply_toolkit!
|
|
81
88
|
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# returns the ask's Pending, which the turn joins before apply_toolkit!
|
|
87
|
-
# (#settle_toolkit_suggestion), with the turn's controller as the
|
|
88
|
-
# request's cancel controller.
|
|
89
|
-
# @return [ToolkitSuggest::Pending, nil] nil unless the turn joins an ask
|
|
90
|
-
def start_toolkit_suggestion(turn)
|
|
91
|
-
mode = Config.get("toolkit.suggest.mode").to_s
|
|
92
|
-
return if mode == "off"
|
|
93
|
-
|
|
94
|
-
joined = mode == "on"
|
|
95
|
-
session = turn.session
|
|
96
|
-
default = Toolkit.of(session.toolkit)
|
|
97
|
-
log = toolkit_suggestion_log
|
|
98
|
-
# The predicate checks the session first; the kits (memory kits are
|
|
99
|
-
# read from disk) and the log (read whole) only when it gets there.
|
|
100
|
-
kits = nil
|
|
101
|
-
candidates = nil
|
|
102
|
-
return unless ToolkitSuggest.eligible?(
|
|
103
|
-
session: session, prompt: turn.prompt, author: turn.origin.author,
|
|
104
|
-
candidate_count: lambda {
|
|
105
|
-
kits = @toolkit_binding.kits
|
|
106
|
-
candidates = ToolkitSuggest.candidates(kits, default_name: default.name)
|
|
107
|
-
candidates.size
|
|
108
|
-
},
|
|
109
|
-
last_suggestion: -> { ToolkitSuggest.last_suggestion(log, session.id) },
|
|
110
|
-
defer_turns: Config.get("toolkit.suggest.defer_turns"),
|
|
111
|
-
defer_tokens: Config.get("toolkit.suggest.defer_tokens")
|
|
112
|
-
)
|
|
89
|
+
# System 1's toolkit suggestion (TurnStages::ToolkitSuggestion):
|
|
90
|
+
# prepare_turn starts, settles and abandons it; the Worker warms it.
|
|
91
|
+
attr_reader :toolkit_suggestion
|
|
92
|
+
private :toolkit_suggestion
|
|
113
93
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
Log.info(:toolkit, "toolkit_suggest_in_flight_skipped", session_id: session.id)
|
|
118
|
-
return
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
client = system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :toolkit),
|
|
122
|
-
ToolkitSuggest::JUDGMENT, session, mode: Config.get("toolkit.suggest.mode").to_s)
|
|
123
|
-
return unless client
|
|
124
|
-
|
|
125
|
-
# The warm-up's only value is the cache: the real ask never queues
|
|
126
|
-
# behind it.
|
|
127
|
-
stop_toolkit_warmup
|
|
128
|
-
|
|
129
|
-
# Rules, not tool lists: this runs before await_init_tasks and
|
|
130
|
-
# apply_staged_tools!, so a first turn's plugin/MCP tools aren't
|
|
131
|
-
# registered yet; the report expands the rules (ToolkitSuggest.rules).
|
|
132
|
-
snapshots = candidates.map do |kit|
|
|
133
|
-
ToolkitSuggest::Candidate.new(name: kit.name.dup.freeze, description: kit.description.dup.freeze,
|
|
134
|
-
rules: ToolkitSuggest.rules(Toolkit.resolve_kit(kit.name, kits)))
|
|
135
|
-
end.freeze
|
|
136
|
-
default_snapshot = ToolkitSuggest::Candidate.new(name: default.name.dup.freeze, description: nil,
|
|
137
|
-
rules: ToolkitSuggest.rules(default.resolve(kits)))
|
|
138
|
-
registered = @tools.entries(toolkit: nil).reject { |entry| entry.source == Tools::ToolIndex::SOURCE }
|
|
139
|
-
core_tools, plugin_tools = registered.partition(&:core?).map { |entries| entries.map { |entry| entry.name.dup.freeze }.freeze }
|
|
140
|
-
state = ToolkitSuggest.question(
|
|
141
|
-
default: default, kits: kits, project: File.basename(session.project_root.to_s),
|
|
142
|
-
prompt: turn.prompt
|
|
143
|
-
).last
|
|
144
|
-
snapshot = ToolkitSuggest::Snapshot.new(
|
|
145
|
-
session_id: session.id.dup.freeze, turn_number: ToolkitSuggest.turn_number(session.messages),
|
|
146
|
-
default: default_snapshot, candidates: snapshots, core_tools: core_tools, plugin_tools: plugin_tools,
|
|
147
|
-
state: { project: state[:project].dup.freeze, prompt: state[:prompt].dup.freeze }.freeze
|
|
148
|
-
)
|
|
149
|
-
threshold = Config.get("toolkit.suggest.threshold")
|
|
150
|
-
runs = session.scratch ? nil : system_one_runs
|
|
151
|
-
pending = ToolkitSuggest::Pending.new(log: log, joined: joined,
|
|
152
|
-
acted: runs && ->(judged, did) { runs.acted(judged.fields[:run_id], did) },
|
|
153
|
-
timeout_seconds: SystemOne::Client.configured_timeout_ms / 1000.0)
|
|
154
|
-
# On mode: cut by a Stop while the turn waits (Pending#wait), not
|
|
155
|
-
# by one after it went on; audit's ask is never cut.
|
|
156
|
-
controller = joined ? pending.cancel_controller : nil
|
|
157
|
-
@side_judgments.start(:toolkit, warn: ToolkitSuggest.method(:warn), finish: pending.method(:finish)) do
|
|
158
|
-
ToolkitSuggest.judge(client: client, snapshot: snapshot, threshold: threshold,
|
|
159
|
-
cancel_controller: controller)
|
|
160
|
-
end
|
|
161
|
-
joined ? pending : nil
|
|
162
|
-
rescue StandardError => e
|
|
163
|
-
ToolkitSuggest.warn(e)
|
|
164
|
-
nil
|
|
165
|
-
end
|
|
166
|
-
private :start_toolkit_suggestion
|
|
167
|
-
|
|
168
|
-
# On mode, before apply_toolkit!: wait for the ask (#start_toolkit_
|
|
169
|
-
# suggestion) up to System 1's timeout from its start, or until a Stop,
|
|
170
|
-
# and put the session on a candidate picked with confidence ≥ threshold
|
|
171
|
-
# (ToolkitBinding#suggest: origin suggested, saved, the notice). Any
|
|
172
|
-
# other outcome (the default, below threshold, undecided, no judgment,
|
|
173
|
-
# a timeout, a Stop) leaves the session on its default. Nothing here
|
|
174
|
-
# sends a request; prepare_turn's cancel check ends a stopped turn. A
|
|
175
|
-
# Stop wins over an answer already in (Pending#wait checks it first).
|
|
176
|
-
def settle_toolkit_suggestion(turn, pending)
|
|
177
|
-
return if pending.nil?
|
|
178
|
-
|
|
179
|
-
outcome, judged = pending.wait(turn.controller)
|
|
180
|
-
if outcome == :timeout
|
|
181
|
-
# The session starts on its default without a word otherwise: a
|
|
182
|
-
# slow System 1 host (a local one) is the usual cause.
|
|
183
|
-
timeout_ms = SystemOne::Client.configured_timeout_ms
|
|
184
|
-
Log.warn(:toolkit, "toolkit_suggest_timeout", session_id: turn.session.id, timeout_ms: timeout_ms,
|
|
185
|
-
reason: "System 1 gave no answer within system_one.timeout_ms")
|
|
186
|
-
@toolkit_binding.fell_back(turn.session, "System 1 took over #{timeout_ms} ms")
|
|
187
|
-
return
|
|
188
|
-
end
|
|
189
|
-
unless outcome == :answered
|
|
190
|
-
Log.info(:toolkit, "toolkit_suggest_#{outcome}", session_id: turn.session.id)
|
|
191
|
-
return
|
|
192
|
-
end
|
|
193
|
-
return if judged.nil?
|
|
194
|
-
|
|
195
|
-
pending.record(toolkit_suggestion_outcome(turn.session, judged))
|
|
196
|
-
rescue StandardError => e
|
|
197
|
-
ToolkitSuggest.warn(e)
|
|
198
|
-
pending&.record("failed")
|
|
199
|
-
end
|
|
200
|
-
private :settle_toolkit_suggestion
|
|
201
|
-
|
|
202
|
-
# What the turn does with an answered ask, as the log line's did:
|
|
203
|
-
# suggested (switched), chosen (the toolkit isn't the default any
|
|
204
|
-
# more), unavailable (the kit is gone), defer or default.
|
|
205
|
-
def toolkit_suggestion_outcome(session, judged)
|
|
206
|
-
case judged.would
|
|
207
|
-
when "suggested"
|
|
208
|
-
return "chosen" unless session.toolkit_origin == "default"
|
|
209
|
-
return "unavailable" unless @toolkit_binding.kits.key?(judged.suggestion.kit)
|
|
210
|
-
|
|
211
|
-
switched = @toolkit_binding.suggest(session, judged.suggestion.kit, confidence: judged.suggestion.confidence,
|
|
212
|
-
deferred: ToolkitSuggest.model_reply?(session.messages))
|
|
213
|
-
switched ? "suggested" : "chosen"
|
|
214
|
-
when "defer" then "defer"
|
|
215
|
-
when "no_judgment"
|
|
216
|
-
# System 1 failed (its own warn) or answered nothing usable.
|
|
217
|
-
@toolkit_binding.fell_back(session, "System 1 gave no answer")
|
|
218
|
-
"default"
|
|
219
|
-
else "default"
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
private :toolkit_suggestion_outcome
|
|
223
|
-
|
|
224
|
-
# On mode, as an idle worker starts (Worker#start_background): one ask
|
|
225
|
-
# with the real question and a stand-in prompt, on its own thread,
|
|
226
|
-
# never logged and never waited for, so System 1's server has the
|
|
227
|
-
# question's prefix cached for the first prompt. Only for a session a
|
|
228
|
-
# first prompt could be asked for: origin default, no model reply, not
|
|
229
|
-
# a delegate child or a review session, at least two candidates; and
|
|
230
|
-
# not when the worker starts on a first prompt (that ask comes at once).
|
|
231
|
-
def warm_toolkit_suggestion!
|
|
232
|
-
return unless Config.get("toolkit.suggest.mode").to_s == "on"
|
|
233
|
-
|
|
234
|
-
session = @session
|
|
235
|
-
return if session.nil? || session.toolkit_origin != "default" || session.delegate? || session.review?
|
|
236
|
-
return if ToolkitSuggest.model_reply?(session.messages) || !session.last_prompt.to_s.strip.empty?
|
|
237
|
-
|
|
238
|
-
default = Toolkit.of(session.toolkit)
|
|
239
|
-
kits = @toolkit_binding.kits
|
|
240
|
-
return if ToolkitSuggest.candidates(kits, default_name: default.name).size < 2
|
|
241
|
-
|
|
242
|
-
client = SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :toolkit)
|
|
243
|
-
return unless client
|
|
244
|
-
|
|
245
|
-
project = File.basename(session.project_root.to_s)
|
|
246
|
-
@toolkit_warmup_thread = Thread.new do
|
|
247
|
-
ToolkitSuggest.warm(client: client, default: default, kits: kits, project: project)
|
|
248
|
-
end
|
|
249
|
-
rescue StandardError => e
|
|
250
|
-
ToolkitSuggest.warn(e)
|
|
251
|
-
nil
|
|
252
|
-
end
|
|
94
|
+
# On mode, as an idle worker starts (Worker#start_background): the
|
|
95
|
+
# warm-up ask (TurnStages::ToolkitSuggestion#warm!).
|
|
96
|
+
def warm_toolkit_suggestion! = @toolkit_suggestion.warm!(@session)
|
|
253
97
|
public :warm_toolkit_suggestion!
|
|
254
98
|
|
|
255
|
-
#
|
|
256
|
-
#
|
|
257
|
-
def
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
SideJudgments.log(ToolkitSuggest.log_path(session_state_dir), tag: :toolkit, event: "toolkit_suggest_log_failed")
|
|
99
|
+
# The stage over this Engine's side asks, binding and tools (built in
|
|
100
|
+
# #build_tools_and_plugins, before the plugins load).
|
|
101
|
+
def build_toolkit_suggestion
|
|
102
|
+
TurnStages::ToolkitSuggestion.new(
|
|
103
|
+
side_judgments: @side_judgments, host_registry: @host_registry, toolkit_binding: @toolkit_binding,
|
|
104
|
+
tools: @tools, state_dir: -> { session_state_dir },
|
|
105
|
+
recording: ->(client, judgment, session, mode:) { system_one_recording(client, judgment, session, mode: mode) },
|
|
106
|
+
runs: -> { system_one_runs }
|
|
107
|
+
)
|
|
265
108
|
end
|
|
266
|
-
private :
|
|
109
|
+
private :build_toolkit_suggestion
|
|
267
110
|
end
|
|
268
111
|
end
|
|
269
112
|
end
|
|
@@ -117,7 +117,7 @@ module Samagotchi
|
|
|
117
117
|
# a display version of the answer (AnswerDisplay) and event[:refs]
|
|
118
118
|
# for refs on it (AnswerRefs).
|
|
119
119
|
session = turn.session
|
|
120
|
-
|
|
120
|
+
@objectives_stage.outgoing(turn, result) unless canceled || result.exhausted?
|
|
121
121
|
display = AnswerDisplay.new(session.messages)
|
|
122
122
|
refs = AnswerRefs.new(session.messages, cwd: session.working_directory)
|
|
123
123
|
after_turn = { type: :after_turn, status: canceled ? "canceled" : "completed",
|
|
@@ -5,8 +5,9 @@ module Samagotchi
|
|
|
5
5
|
# What a turn's steps use before and around the model: its settings,
|
|
6
6
|
# images and vision, the model ref and resumed run start it generates
|
|
7
7
|
# with, the used memories it publishes, and the turn-end warm-up of the
|
|
8
|
-
# next one. Writes @next_turn_waiting;
|
|
9
|
-
# @prompt_warmup, @thinking_notices,
|
|
8
|
+
# next one (TurnStages::Warmup, built here). Writes @next_turn_waiting;
|
|
9
|
+
# reads @model_settings, @kernel, @prompt_warmup, @thinking_notices,
|
|
10
|
+
# @turn_state, @turn_warmup.
|
|
10
11
|
module TurnHelpersPart
|
|
11
12
|
# The turn's settings for the kernel (LLM::TurnSettings): vision,
|
|
12
13
|
# sampling and thinking, the thinking resolved before the hooks run.
|
|
@@ -34,44 +35,20 @@ module Samagotchi
|
|
|
34
35
|
# warm-up then. nil: neither.
|
|
35
36
|
attr_accessor :next_turn_waiting
|
|
36
37
|
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return if reminders_due? || @turn_state.carried_steers? || @next_turn_waiting&.call
|
|
50
|
-
|
|
51
|
-
messages = ContextNote.with_system_head(turn.session.messages.dup, { role: "system", content: system_prompt(model) })
|
|
52
|
-
# Under the next turn's strategy (a /model switch may have changed it);
|
|
53
|
-
# the last turn's when it can't be resolved.
|
|
54
|
-
prompt, images = @kernel.warmup_prompt(messages, llm_context: @model_settings.turn_llm_context || @kernel.turn_settings&.llm_context)
|
|
55
|
-
return unless prompt
|
|
56
|
-
|
|
57
|
-
started = @prompt_warmup.start(client: client, prompt: prompt, model: @kernel.turn_settings.model_name,
|
|
58
|
-
slot: @kernel.last_slot, images: images)
|
|
59
|
-
@kernel.warmed_up! if started
|
|
60
|
-
rescue StandardError => e
|
|
61
|
-
Log.warn(:model, "warmup_skipped", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
62
|
-
end
|
|
63
|
-
private :warm_up_next_turn
|
|
64
|
-
|
|
65
|
-
# Never a paid or remote host (cache.warmup auto means local only), nor
|
|
66
|
-
# the chat loop (Splash keeps its own cache; llama.cpp's chat path is
|
|
67
|
-
# not measured), nor a client that can't warm up (a spec's stub).
|
|
68
|
-
def warmup_target?(model, client)
|
|
69
|
-
return false if model.chat? || model.entry.remote?
|
|
70
|
-
return false unless client.respond_to?(:warm_up)
|
|
71
|
-
|
|
72
|
-
PromptWarmup.enabled?
|
|
38
|
+
# The turn-end warm-up's stage (TurnStages::Warmup).
|
|
39
|
+
attr_reader :turn_warmup
|
|
40
|
+
private :turn_warmup
|
|
41
|
+
|
|
42
|
+
# The stage over the kernel and the Engine's PromptWarmup (built in
|
|
43
|
+
# #initialize, once the kernel is).
|
|
44
|
+
def build_turn_warmup
|
|
45
|
+
TurnStages::Warmup.new(
|
|
46
|
+
prompt_warmup: @prompt_warmup, kernel: @kernel, turn_state: @turn_state, model_settings: @model_settings,
|
|
47
|
+
model: -> { effective_model }, system_prompt: ->(model) { system_prompt(model) },
|
|
48
|
+
reminders_due: -> { reminders_due? }, next_turn_waiting: -> { next_turn_waiting&.call }
|
|
49
|
+
)
|
|
73
50
|
end
|
|
74
|
-
private :
|
|
51
|
+
private :build_turn_warmup
|
|
75
52
|
|
|
76
53
|
# The ref (host:model, as asked) a turn's generations ran on: the model
|
|
77
54
|
# #generate resolved, after the before_turn hooks (which may switch it).
|