samagotchi 0.154.1 → 0.156.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 +29 -1
- data/bin/chi +1 -1
- data/docs/architecture.md +12 -7
- data/docs/bundles.md +21 -0
- data/docs/cli.md +1 -1
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +5 -5
- data/docs/internals/attached-context.md +9 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/engine.md +642 -831
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +13 -6
- data/docs/internals/remote-sessions.md +3 -1
- data/docs/internals/web-frontend.md +1 -1
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +4 -4
- data/docs/system-one.md +3 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +7 -51
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/cli/parent_wait.rb +1 -1
- data/lib/samagotchi/config.rb +17 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_window.rb +3 -3
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/engine/context_judge.rb +21 -119
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +30 -80
- 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 +26 -13
- data/lib/samagotchi/engine/toolkit.rb +35 -191
- 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 +166 -98
- data/lib/samagotchi/engine_peers.rb +1 -1
- 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 +28 -11
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +1 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +215 -313
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +87 -49
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/llm_context_notice.rb +1 -1
- data/lib/samagotchi/llm_context_strategy.rb +7 -9
- data/lib/samagotchi/llm_context_view.rb +9 -0
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/model_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +6 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/note_command.rb +1 -2
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/plugin/sessions.rb +2 -5
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +3 -3
- data/lib/samagotchi/reminder_carry.rb +1 -1
- data/lib/samagotchi/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +3 -3
- data/lib/samagotchi/self_report.rb +7 -7
- data/lib/samagotchi/session.rb +50 -7
- data/lib/samagotchi/session_access/local.rb +7 -5
- data/lib/samagotchi/session_access/remote.rb +17 -3
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +138 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +189 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +26 -1540
- data/lib/samagotchi/session_metrics.rb +1 -1
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +3 -3
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/system_prompt.rb +37 -30
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +2 -6
- 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/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +3 -3
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +15 -7
- data/lib/samagotchi/tool_declarations.rb +6 -6
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +96 -102
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/delegate.rb +10 -5
- data/lib/samagotchi/tools/delegate_result.rb +4 -4
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/registry.rb +64 -44
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +150 -102
- data/lib/samagotchi/tools/tool_surface.rb +187 -0
- data/lib/samagotchi/turn_context.rb +68 -0
- data/lib/samagotchi/turn_stages/context_judge.rb +172 -0
- data/lib/samagotchi/turn_stages/objectives.rb +150 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +257 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +168 -0
- data/lib/samagotchi/turn_stages/warmup.rb +82 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +3 -3
- data/lib/samagotchi/web/app.rb +15 -8
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +1 -1
- data/lib/samagotchi/web/participant_access.rb +2 -2
- 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/composer_fold.js +3 -0
- 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/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 +3 -3
- data/lib/samagotchi/web/session_control.rb +3 -3
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +3 -3
- data/lib/samagotchi/web/session_summary.rb +3 -4
- data/lib/samagotchi/worker.rb +55 -27
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +29 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "builtin_calls"
|
|
4
|
+
require_relative "deferred_tools"
|
|
5
|
+
require_relative "registry"
|
|
6
|
+
|
|
7
|
+
module Samagotchi
|
|
8
|
+
module Tools
|
|
9
|
+
# What one turn's loops see of the session's tools, as one frozen value:
|
|
10
|
+
# what the request declares, what call_tool reaches, what the toolkit
|
|
11
|
+
# leaves out (and why), and the dispatch decision for a call. Built once
|
|
12
|
+
# per turn in Engine#generate from the Registry (its only input), where
|
|
13
|
+
# the turn's LLM context layers and its host's loads_tools are known,
|
|
14
|
+
# and carried as TurnContext#surface: KernelLoop#dispatch, ChatLoop's
|
|
15
|
+
# tool_definitions/deferred_tools and ToolRunner's left-out, unwrap
|
|
16
|
+
# and refusal checks read it. A change made while the turn runs (a
|
|
17
|
+
# /toolkit change, chi.replace_tools: staged anyway) shapes the next
|
|
18
|
+
# turn's surface only. Outside a turn a caller builds one (.for): a bare
|
|
19
|
+
# kernel or loop at its own context (KernelLoop#own_context), the tool
|
|
20
|
+
# index at load and at #refresh!, the readers off the turn thread from
|
|
21
|
+
# ToolkitBinding#published.
|
|
22
|
+
#
|
|
23
|
+
# +declared+ the entries the request declares, in prompt order (Registry#entries)
|
|
24
|
+
# +index_only+ the entries the toolkit leaves out that call_tool reaches (Registry#reachable_names)
|
|
25
|
+
# +left_out+ {name => why} for every tool the toolkit leaves out (Registry#left_out_names),
|
|
26
|
+
# in declaration order: :indexed (call_tool reaches it) or :user_only (only
|
|
27
|
+
# /toolkit add brings it back: removed, or nothing reaches it)
|
|
28
|
+
# +removed+ the names the toolkit removes (Registry#removed_names: remove:, --no-tool,
|
|
29
|
+
# /toolkit remove), in declaration order
|
|
30
|
+
# +stranded+ the names of the plugin tools registered after the index was declared that
|
|
31
|
+
# the toolkit leaves out and nothing reaches (Registry#stranded_names)
|
|
32
|
+
# +registered+ {name => entry} of every registered tool, declared or not
|
|
33
|
+
# +toolkit+ the session's Toolkit::Resolved (nil: full)
|
|
34
|
+
# +reaching_kit+ group → the kit that reaches it (Registry#reaching_kit), for a refusal's hint
|
|
35
|
+
# +index+ the tool index's declaration (Registry::Index), nil without one
|
|
36
|
+
# +layers+ the turn's LLM context layers
|
|
37
|
+
# +loads_tools+ whether the turn's host loads tools (TurnSettings#loads_tools)
|
|
38
|
+
class ToolSurface < Data.define(:declared, :index_only, :left_out, :removed, :stranded, :registered, :toolkit,
|
|
39
|
+
:reaching_kit, :index, :layers, :loads_tools)
|
|
40
|
+
# @param registry [Registry, nil] nil: no tools at all
|
|
41
|
+
# @param layers [Array<Symbol>] the turn's LLM context layers
|
|
42
|
+
# @param loads_tools [Boolean] the turn's host's (TurnSettings#loads_tools)
|
|
43
|
+
# @param toolkit [Toolkit::Resolved, nil] the toolkit to read the
|
|
44
|
+
# Registry under: its own (the session's) by default; another one
|
|
45
|
+
# for what a change would declare (ToolkitBinding#explained)
|
|
46
|
+
# @return [ToolSurface]
|
|
47
|
+
def self.for(registry, layers: [], loads_tools: false, toolkit: registry&.toolkit)
|
|
48
|
+
layers = Array(layers)
|
|
49
|
+
return empty(layers, loads_tools) if registry.nil?
|
|
50
|
+
|
|
51
|
+
reachable = registry.reachable_names(layers: layers, toolkit: toolkit)
|
|
52
|
+
new(declared: registry.entries(layers: layers, toolkit: toolkit),
|
|
53
|
+
index_only: reachable.map { |name| registry[name] },
|
|
54
|
+
left_out: registry.left_out_names(layers: layers, toolkit: toolkit)
|
|
55
|
+
.to_h { |name| [name, reachable.include?(name) ? :indexed : :user_only] },
|
|
56
|
+
removed: registry.removed_names(layers: layers, toolkit: toolkit),
|
|
57
|
+
stranded: registry.stranded_names(layers: layers, toolkit: toolkit),
|
|
58
|
+
registered: registry.registered.to_h { |entry| [entry.name, entry] },
|
|
59
|
+
toolkit: toolkit, reaching_kit: registry.reaching_kit, index: registry.index,
|
|
60
|
+
layers: layers, loads_tools: loads_tools == true)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Its lists frozen (copies of the ones given), whichever way it is
|
|
64
|
+
# made (.for, .new, #with): no reader can change a turn's surface.
|
|
65
|
+
# The entries in them stay the Registry's own.
|
|
66
|
+
def initialize(declared:, index_only:, left_out:, registered:, layers:, removed: [], stranded: [], **)
|
|
67
|
+
super(declared: frozen_copy(Array(declared)), index_only: frozen_copy(Array(index_only)),
|
|
68
|
+
left_out: frozen_copy(left_out.to_h), removed: frozen_copy(Array(removed)),
|
|
69
|
+
stranded: frozen_copy(Array(stranded)), registered: frozen_copy(registered.to_h),
|
|
70
|
+
layers: frozen_copy(Array(layers)), **)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The turn's surface (+context+'s: an Engine turn's, or a bare kernel's
|
|
74
|
+
# or loop's own, KernelLoop#own_context); a context without one raises
|
|
75
|
+
# MissingSurface (no reader falls back to the Registry as it is now).
|
|
76
|
+
# @param context [TurnContext]
|
|
77
|
+
# @return [ToolSurface]
|
|
78
|
+
def self.of(context) = context.surface || raise(MissingSurface)
|
|
79
|
+
|
|
80
|
+
# A bare kernel's or loop's own +context+ (no Engine turn: a spec, a
|
|
81
|
+
# bench) with a surface: its own, else one built now from +registry+
|
|
82
|
+
# under its layers and loads_tools. Only their own default context
|
|
83
|
+
# goes through here (KernelLoop#own_context, ChatLoop#turn_context);
|
|
84
|
+
# a context handed down is read as it is (.of).
|
|
85
|
+
# @param context [TurnContext]
|
|
86
|
+
# @return [TurnContext]
|
|
87
|
+
def self.bare(context, registry)
|
|
88
|
+
return context if context.surface
|
|
89
|
+
|
|
90
|
+
context.with(surface: self.for(registry, layers: context.llm_context_layers,
|
|
91
|
+
loads_tools: context.settings&.loads_tools == true))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.empty(layers, loads_tools)
|
|
95
|
+
new(declared: [], index_only: [], left_out: {}, removed: [], stranded: [], registered: {}, toolkit: nil,
|
|
96
|
+
reaching_kit: nil, index: nil, layers: layers, loads_tools: loads_tools == true)
|
|
97
|
+
end
|
|
98
|
+
private_class_method :empty
|
|
99
|
+
|
|
100
|
+
# @return [Array<String>] the declared tools' names, in prompt order
|
|
101
|
+
def declared_names = declared.map(&:name)
|
|
102
|
+
|
|
103
|
+
# @return [Array<Hash>] the declared tools' schemas, in prompt order
|
|
104
|
+
def schemas = declared.map(&:schema)
|
|
105
|
+
|
|
106
|
+
# The declared entry +name+ names (the one a call dispatches to), nil
|
|
107
|
+
# otherwise.
|
|
108
|
+
# @return [Registry::Entry, nil]
|
|
109
|
+
def declared_entry(name) = declared.find { |entry| entry.name == name.to_s }
|
|
110
|
+
|
|
111
|
+
# The entry +name+ names when call_tool reaches it (#index_only).
|
|
112
|
+
# @return [Registry::Entry, nil]
|
|
113
|
+
def indexed_entry(name) = index_only.find { |entry| entry.name == name.to_s }
|
|
114
|
+
|
|
115
|
+
# Whether the toolkit leaves +name+'s real tool out (#left_out).
|
|
116
|
+
def left_out?(name) = left_out.key?(name.to_s)
|
|
117
|
+
|
|
118
|
+
# Whether any tool, declared or not, is registered as +name+.
|
|
119
|
+
def registered?(name) = registered.key?(name.to_s)
|
|
120
|
+
|
|
121
|
+
# The entry a call runs on, nil when it can't run in this turn: one
|
|
122
|
+
# reached through call_tool (via:) only while the toolkit reaches it
|
|
123
|
+
# that way; any other by its name when declared, or, on a host that
|
|
124
|
+
# loads tools, when call_tool reaches it.
|
|
125
|
+
# @param call [Hash] the normalized call ({name:, via:, …})
|
|
126
|
+
# @return [Registry::Entry, nil]
|
|
127
|
+
def entry_for(call)
|
|
128
|
+
name = call[:name]
|
|
129
|
+
return indexed_entry(name) if call[:via]
|
|
130
|
+
|
|
131
|
+
declared_entry(name) || (loads_tools ? indexed_entry(name) : nil)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Shell-like names (the harness's is `execute`). The unknown-tool error
|
|
135
|
+
# never echoes the name the model used: a model that sees its own wrong
|
|
136
|
+
# name again calls it again (a steer that named `bash` raised one
|
|
137
|
+
# model's bash rate from 9% to 30%); the activity event and the logs
|
|
138
|
+
# keep it.
|
|
139
|
+
SHELL_LIKE_TOOL_NAMES = %w[
|
|
140
|
+
bash sh zsh shell terminal cmd command run run_command run_shell exec execute_command
|
|
141
|
+
].freeze
|
|
142
|
+
|
|
143
|
+
# The error a call #entry_for has no entry for gets back. A shell-like
|
|
144
|
+
# name with `execute` declared points at `execute`; every other unknown
|
|
145
|
+
# name lists what is declared. Neither echoes the wrong name.
|
|
146
|
+
#
|
|
147
|
+
# A tool the session's toolkit leaves out is a real one, so its error
|
|
148
|
+
# does name it (deliberately, against the rule above): the model learns
|
|
149
|
+
# it exists, and how to reach it (call_tool, when the toolkit reaches
|
|
150
|
+
# it that way) or that only the user can load it. A call reached
|
|
151
|
+
# through call_tool (+via+) of a tool the toolkit declares is told to
|
|
152
|
+
# call it directly: the model's call_tool(tool: "chi/read") with read
|
|
153
|
+
# declared, or a hook's rewrite (a direct call can't carry via:).
|
|
154
|
+
# @return [String]
|
|
155
|
+
def unknown_tool_error(name, via: nil)
|
|
156
|
+
names = declared_names
|
|
157
|
+
in_toolkit = " in this session#{" (toolkit #{toolkit.name})" if toolkit}"
|
|
158
|
+
if via && declared_entry(name)
|
|
159
|
+
"Error: #{name} is declared#{in_toolkit}; call it directly. Available: #{names.join(", ")}"
|
|
160
|
+
elsif !via && indexed_entry(name)
|
|
161
|
+
"Error: #{name} isn't declared#{in_toolkit}; call it with #{BuiltinCalls::VIA}(tool: \"chi/#{name}\", args: {…}) " \
|
|
162
|
+
"(find_tools gives its inputSchema). Available: #{names.join(", ")}"
|
|
163
|
+
elsif left_out?(name)
|
|
164
|
+
"Error: #{name} isn't loaded#{in_toolkit} and can't be loaded from " \
|
|
165
|
+
"here; only the user can (/toolkit add #{name}). Available: #{names.join(", ")}"
|
|
166
|
+
elsif SHELL_LIKE_TOOL_NAMES.include?(name.to_s.downcase) && names.include?("execute")
|
|
167
|
+
"Error: no such tool. Shell commands run with `execute` (same arguments)."
|
|
168
|
+
else
|
|
169
|
+
"Error: no such tool. Available: #{names.join(", ")}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# The tools a request on a host that loads tools declares deferred,
|
|
174
|
+
# +conversation+ (the request's, engine format) naming the ones it
|
|
175
|
+
# loaded (Tools::DeferredTools.list). The index's source tools are
|
|
176
|
+
# asked now (a late MCP server joins the list mid-turn, as before).
|
|
177
|
+
# @return [Array<DeferredTools::Deferred>]
|
|
178
|
+
def deferred_for(conversation) = DeferredTools.list(self, conversation)
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
# +list+ itself when frozen, else a frozen copy (the caller's stays
|
|
183
|
+
# as it is).
|
|
184
|
+
def frozen_copy(list) = list.frozen? ? list : list.dup.freeze
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "turn_origin"
|
|
4
|
+
require_relative "steer"
|
|
5
|
+
require_relative "guardrails/context"
|
|
6
|
+
require_relative "llm/turn_settings"
|
|
7
|
+
|
|
8
|
+
module Samagotchi
|
|
9
|
+
# What one turn's tool path reads of it, as one frozen value: built by
|
|
10
|
+
# Engine#begin_turn before the turn is published (TurnState#begin!),
|
|
11
|
+
# handed explicitly down the loops (ModelBackend#complete's context:,
|
|
12
|
+
# KernelLoop#run, ChatLoop#complete), ToolResponse.run_batch,
|
|
13
|
+
# ToolRunner#run, Guardrails::Gate#evaluate and KernelLoop#dispatch_tool_call
|
|
14
|
+
# (its ToolContext). Lifetime: one turn. The source of truth for the
|
|
15
|
+
# turn's id, session, origin and controller (Engine::Turn keeps only the
|
|
16
|
+
# lifecycle state and reads these from it).
|
|
17
|
+
#
|
|
18
|
+
# +id+ the turn's id (:turn_started's turn_id)
|
|
19
|
+
# +session+ the Session it runs in (live: its working-set pins are read at call time)
|
|
20
|
+
# +origin+ its TurnOrigin (TurnOrigin::NONE for the owner's)
|
|
21
|
+
# +author+ the participant's author ("p:<id>") when one wrote it, else nil
|
|
22
|
+
# +participant_mode+ a participant's turn's mode (ParticipantMode: :review, :pair), nil for the owner's
|
|
23
|
+
# +participant+ that participant's Participants::Entry (participants.json), nil for none
|
|
24
|
+
# +controller+ the turn's CancellationController (live: its cancel state); a thread
|
|
25
|
+
# off the turn cancels through TurnState#controller, never through this
|
|
26
|
+
# +cwd+ the session's working directory (nil: the process's, Dir.pwd)
|
|
27
|
+
# +session_dir+ the session's folder (a cut tool output is saved there), nil for none
|
|
28
|
+
# +state_dir+ the sessions' state dir
|
|
29
|
+
# +git+ the turn's Guardrails::GitInfo (asked once per directory per turn)
|
|
30
|
+
# +settings+ the turn's LLM::TurnSettings: nil until Engine#generate sets them
|
|
31
|
+
# +model_key+, +fallback_model_key+ the memory overlay keys (memory_read, memory_write,
|
|
32
|
+
# rules' models:), +model_name+ the bare model name the gate's rules match: frozen at
|
|
33
|
+
# Engine#generate with the settings (a before_turn hook may switch the model)
|
|
34
|
+
# +surface+ the turn's Tools::ToolSurface (what its loops declare, defer and dispatch):
|
|
35
|
+
# nil until Engine#generate builds it; a reader given none raises Tools::MissingSurface
|
|
36
|
+
# (ToolSurface.of), a bare kernel's or loop's own context builds one (ToolSurface.bare)
|
|
37
|
+
#
|
|
38
|
+
# Only the turn thread replaces it (TurnState#replace_context), one frozen
|
|
39
|
+
# value per write; other threads read the published one (Engine#turn_context,
|
|
40
|
+
# Engine#last_turn_context).
|
|
41
|
+
TurnContext = Data.define(:id, :session, :origin, :author, :participant_mode, :participant, :controller, :cwd,
|
|
42
|
+
:session_dir, :state_dir, :git, :settings, :model_key, :fallback_model_key, :model_name,
|
|
43
|
+
:surface) do
|
|
44
|
+
# A context outside an Engine turn (a bare kernel, a spec, the bundle
|
|
45
|
+
# test kit's Gate): no session, the owner's, no controller, a fresh
|
|
46
|
+
# GitInfo, the settings given (LLM::TurnSettings.none by default).
|
|
47
|
+
# @return [TurnContext]
|
|
48
|
+
def self.none(**fields)
|
|
49
|
+
new(id: nil, session: nil, origin: TurnOrigin::NONE, author: nil, participant_mode: nil, participant: nil,
|
|
50
|
+
controller: nil, cwd: nil, session_dir: nil, state_dir: nil, git: Guardrails::GitInfo.new,
|
|
51
|
+
settings: LLM::TurnSettings.none, model_key: nil, fallback_model_key: nil, model_name: nil, surface: nil,
|
|
52
|
+
**fields)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Whether a participant wrote the turn (Steer.participant_author?).
|
|
56
|
+
def participant? = Steer.participant_author?(author)
|
|
57
|
+
|
|
58
|
+
# Whether the turn's controller is cancelled (a Stop, a hook's stop_turn).
|
|
59
|
+
def cancelled? = !!controller&.cancelled?
|
|
60
|
+
|
|
61
|
+
# The turn's LLM context layers ([] without settings or a strategy): the
|
|
62
|
+
# tools they add are the turn's to offer and dispatch.
|
|
63
|
+
def llm_context_layers = settings&.llm_context&.active_layers || []
|
|
64
|
+
|
|
65
|
+
# The session's working-set pins (Session#working_set_pins), read now.
|
|
66
|
+
def working_set_pins = session.respond_to?(:working_set_pins) ? Array(session.working_set_pins) : []
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../config"
|
|
4
|
+
require_relative "../context_candidate_choice"
|
|
5
|
+
require_relative "../context_candidates"
|
|
6
|
+
require_relative "../context_candidates_log"
|
|
7
|
+
require_relative "../context_judge"
|
|
8
|
+
require_relative "../context_sources"
|
|
9
|
+
require_relative "../log"
|
|
10
|
+
require_relative "../owner_prompt"
|
|
11
|
+
require_relative "../session"
|
|
12
|
+
require_relative "../side_judgments"
|
|
13
|
+
require_relative "../system_one/client"
|
|
14
|
+
|
|
15
|
+
module Samagotchi
|
|
16
|
+
module TurnStages
|
|
17
|
+
# The attached-context candidate judge's stage (Samagotchi::ContextJudge
|
|
18
|
+
# is the judge itself; inside this class the bare name is this stage):
|
|
19
|
+
# its pass at an owner turn's start and for a late candidate, the asks it
|
|
20
|
+
# starts, and the cards an answer brought while a turn ran, shown once it
|
|
21
|
+
# has ended. Its state is those held cards, under their mutex.
|
|
22
|
+
#
|
|
23
|
+
# Its two callables are read on the asks' thread, after the call that
|
|
24
|
+
# started the asks returned (an answer comes when it comes): which
|
|
25
|
+
# session the Engine is on then, and the card shown where cards go then.
|
|
26
|
+
class ContextJudge
|
|
27
|
+
# @param side_judgments [SideJudgments] the Engine's side asks (:context kind)
|
|
28
|
+
# @param host_registry [HostRegistry] System 1's client resolves its host here
|
|
29
|
+
# @param turn_state [TurnState] whether a turn runs as an answer comes (#running?)
|
|
30
|
+
# @param show_card [#call] (**card) shows a card (Engine#show_card: EventRoutes, on the caller's thread)
|
|
31
|
+
# @param current_session [#call] the session the Engine is on as an answer comes (nil: none)
|
|
32
|
+
def initialize(side_judgments:, host_registry:, turn_state:, show_card:, current_session:)
|
|
33
|
+
@side_judgments = side_judgments
|
|
34
|
+
@host_registry = host_registry
|
|
35
|
+
@turn_state = turn_state
|
|
36
|
+
@show_card = show_card
|
|
37
|
+
@current_session = current_session
|
|
38
|
+
# On mode's cards an answer brought while a turn ran: shown once it
|
|
39
|
+
# has ended (#show_cards_after_turn).
|
|
40
|
+
@cards_after_turn = []
|
|
41
|
+
@cards_mutex = Mutex.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# For specs: the asks' thread (SideJudgments' :context kind), nil before any.
|
|
45
|
+
def asks_thread = @side_judgments.thread(:context)
|
|
46
|
+
|
|
47
|
+
# The candidate judge at an owner turn's start, after the toolkit's
|
|
48
|
+
# ask settled: the session's candidates' status changes are logged
|
|
49
|
+
# (ContextCandidatesLog#look) and, unless context.candidates.mode is
|
|
50
|
+
# off, the proposed candidates due an ask are asked about the owner's
|
|
51
|
+
# prompts so far, this one included. Never a participant's turn, a
|
|
52
|
+
# review session or a Continue (no new prompt). The sessions dir is
|
|
53
|
+
# the turn's (TurnContext#state_dir).
|
|
54
|
+
# @param recorder [SystemOne::Recorder] the asks' runs (built for this call)
|
|
55
|
+
def start(turn, recorder:)
|
|
56
|
+
session = turn.session
|
|
57
|
+
return unless turn.prompt.is_a?(String) && OwnerPrompt.turn?(session: session, author: turn.origin.author)
|
|
58
|
+
|
|
59
|
+
pass(session, ContextCandidates.owner_texts(session.messages) + [turn.prompt], phase: "turn",
|
|
60
|
+
state_dir: turn.context.state_dir, recorder: recorder)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A late proposal no owner prompt mentions (Plugin::Host
|
|
64
|
+
# #candidate_proposed: github-pr's init task runs beside the first
|
|
65
|
+
# turn): judged on the owner's prompts so far, the running turn's too.
|
|
66
|
+
# The caller (Engine#judge_late_candidates) gives the session the
|
|
67
|
+
# Engine is on, never nil nor a review session.
|
|
68
|
+
# @param messages [Array<Hash>] the owner's conversation so far (Engine#plugin_messages)
|
|
69
|
+
# @param recorder [SystemOne::Recorder] the asks' runs (built for this call)
|
|
70
|
+
# @param state_dir [String] the sessions dir
|
|
71
|
+
def late_candidates(session, messages:, recorder:, state_dir:)
|
|
72
|
+
texts = ContextCandidates.owner_texts(messages)
|
|
73
|
+
pass(session, texts, phase: "late", state_dir: state_dir, recorder: recorder) unless texts.empty?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The cards #choice held while a turn ran (Engine#run_turn's ensure,
|
|
77
|
+
# after the turn flag is released).
|
|
78
|
+
def show_cards_after_turn
|
|
79
|
+
cards = @cards_mutex.synchronize { @cards_after_turn.slice!(0..) }
|
|
80
|
+
cards.each { |card| @show_card.call(**card) }
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
Samagotchi::ContextJudge.warn(e)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# One look at +session+'s candidates; the asks go to SideJudgments'
|
|
88
|
+
# :context kind, one thread asking each due candidate in turn.
|
|
89
|
+
def pass(session, texts, phase:, state_dir:, recorder:)
|
|
90
|
+
return if session.id.nil? || session.scratch
|
|
91
|
+
|
|
92
|
+
rows = ContextCandidates.with_status(session.id, project_root: session.project_root, state_dir: state_dir)
|
|
93
|
+
return if rows.empty?
|
|
94
|
+
|
|
95
|
+
log = ContextCandidatesLog.log_for(state_dir)
|
|
96
|
+
records = ContextCandidatesLog.records_for(log, session.id)
|
|
97
|
+
ContextCandidatesLog.look(log, session.id, rows, records) if phase == "turn"
|
|
98
|
+
settings = Samagotchi::ContextJudge::Settings.from_config
|
|
99
|
+
return if settings.mode == "off"
|
|
100
|
+
|
|
101
|
+
jobs = Samagotchi::ContextJudge.jobs(rows, records, session_id: session.id, texts: texts, settings: settings,
|
|
102
|
+
phase: phase, parent_attached: parent_attached(session, state_dir))
|
|
103
|
+
return if jobs.empty?
|
|
104
|
+
|
|
105
|
+
client = recorder.wrap(SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :context),
|
|
106
|
+
Samagotchi::ContextJudge::JUDGMENT, session, mode: settings.mode)
|
|
107
|
+
return unless client
|
|
108
|
+
|
|
109
|
+
start_asks(client, jobs, log, settings, choice(session, log, settings, state_dir))
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
Samagotchi::ContextJudge.warn(e)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @return [Thread, nil] the asks' thread, nil when none started
|
|
115
|
+
def start_asks(client, jobs, log, settings, choice)
|
|
116
|
+
if @side_judgments.in_flight?(:context)
|
|
117
|
+
Log.info(:context, "context_judge_in_flight_skipped", session_id: jobs.first.session_id)
|
|
118
|
+
return nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
@side_judgments.start(:context, warn: Samagotchi::ContextJudge.method(:warn)) do
|
|
122
|
+
jobs.each { |job| choice.call(job, Samagotchi::ContextJudge.ask(client: client, job: job, log: log, settings: settings)) }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# What an answer does on the asks' thread (ContextCandidateChoice
|
|
127
|
+
# #after_judged: on mode attaches or cards; audit nothing). Taken on
|
|
128
|
+
# the caller's thread. The attach only writes the session's folder: its
|
|
129
|
+
# note comes with the next between-turn delivery, never into a running
|
|
130
|
+
# turn. A card shows only while the Engine is still on that session,
|
|
131
|
+
# and not in a running turn (it isn't the turn's: no "waiting for you"
|
|
132
|
+
# on it), but once it has ended.
|
|
133
|
+
# @return [#call] (job, fields)
|
|
134
|
+
def choice(session, log, settings, state_dir)
|
|
135
|
+
session_id = session.id
|
|
136
|
+
project_root = session.project_root
|
|
137
|
+
show = lambda do |**card|
|
|
138
|
+
current = @current_session.call
|
|
139
|
+
next nil unless current.nil? || current.id == session_id
|
|
140
|
+
|
|
141
|
+
@cards_mutex.synchronize do
|
|
142
|
+
next @cards_after_turn << card if @turn_state.running?
|
|
143
|
+
|
|
144
|
+
@show_card.call(**card)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
lambda do |job, fields|
|
|
148
|
+
ContextCandidateChoice.after_judged(job: job, fields: fields, settings: settings, project_root: project_root,
|
|
149
|
+
state_dir: state_dir, log: log, show_card: show)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# A delegate child's parent's sources, by title (else name): the
|
|
154
|
+
# judge's started_from. nil for any other session (a chi send --new
|
|
155
|
+
# session has no parent; a fork isn't a delegate).
|
|
156
|
+
def parent_attached(session, state_dir)
|
|
157
|
+
return nil unless session.delegate? && session.parent_id
|
|
158
|
+
|
|
159
|
+
parent_root = if Session.exist?(session.parent_id, state_dir: state_dir)
|
|
160
|
+
Session.load(session.parent_id, state_dir: state_dir).project_root
|
|
161
|
+
else
|
|
162
|
+
session.project_root
|
|
163
|
+
end
|
|
164
|
+
ContextSources.attached(session.parent_id, project_root: parent_root, state_dir: state_dir)
|
|
165
|
+
.map { |attached| attached.source.title || attached.name }
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
Log.warn(:context, "context_parent_attached_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
168
|
+
[]
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require_relative "../log"
|
|
5
|
+
require_relative "../objectives"
|
|
6
|
+
require_relative "../objectives_audit"
|
|
7
|
+
require_relative "../owner_prompt"
|
|
8
|
+
require_relative "../system_one/client"
|
|
9
|
+
|
|
10
|
+
module Samagotchi
|
|
11
|
+
module TurnStages
|
|
12
|
+
# The session's objectives over a turn (Samagotchi::Objectives is the
|
|
13
|
+
# list itself; inside this class the bare name is this stage): System 1's
|
|
14
|
+
# audit of what comes in at a turn's start and what goes out at its end
|
|
15
|
+
# (ObjectivesAudit), the view announced when it changed, and the model's
|
|
16
|
+
# proposals kept as cards until the turn is over. Its state is those
|
|
17
|
+
# proposals under their mutex and the last view announced (under the
|
|
18
|
+
# event lock); the audit is built per call.
|
|
19
|
+
class Objectives
|
|
20
|
+
# @param side_judgments [SideJudgments] the Engine's side asks (the audit's kinds)
|
|
21
|
+
# @param host_registry [HostRegistry] System 1's client resolves its host here
|
|
22
|
+
def initialize(side_judgments:, host_registry:)
|
|
23
|
+
@side_judgments = side_judgments
|
|
24
|
+
@host_registry = host_registry
|
|
25
|
+
# The last view announced (#announce), under the event lock.
|
|
26
|
+
@announced = nil
|
|
27
|
+
# The model's proposals until the turn is over (#finally).
|
|
28
|
+
@proposals = []
|
|
29
|
+
@proposals_mutex = Mutex.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# System 1 on the objectives, in audit (ObjectivesAudit), at a turn's
|
|
33
|
+
# start: the owner's turns only, and for a message a person's: a
|
|
34
|
+
# reminder's or a delegate's prompt isn't one.
|
|
35
|
+
# @param recorder [SystemOne::Recorder] the audit's runs (built for this call)
|
|
36
|
+
def incoming(turn, recorder:)
|
|
37
|
+
return unless turn.prompt.is_a?(String) && turn.origin.human? && OwnerPrompt.turn?(session: turn.session, author: turn.origin.author)
|
|
38
|
+
|
|
39
|
+
audit(turn.context.state_dir, recorder).incoming(turn.session, turn.prompt)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The turn's answer, after the end event and before :after_turn (the
|
|
43
|
+
# caller skips a canceled or exhausted turn): the owner's turns only.
|
|
44
|
+
# @param recorder [SystemOne::Recorder] the audit's runs (built for this
|
|
45
|
+
# call: after a before_turn hook's model switch, the new model)
|
|
46
|
+
def outgoing(turn, result, recorder:)
|
|
47
|
+
return unless OwnerPrompt.turn?(session: turn.session, author: turn.origin.author)
|
|
48
|
+
|
|
49
|
+
audit(turn.context.state_dir, recorder).outgoing(turn.session, result.text)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A note added between turns (Engine#add_context_note): incoming too.
|
|
53
|
+
# @param recorder [SystemOne::Recorder] the audit's runs (built for this call)
|
|
54
|
+
# @param state_dir [String] the sessions dir
|
|
55
|
+
def note(session, note, recorder:, state_dir:) = audit(state_dir, recorder).note(session, note)
|
|
56
|
+
|
|
57
|
+
# The session's objectives as the page draws them (Objectives.view), when
|
|
58
|
+
# that differs from the last announced: after an edit (its file written,
|
|
59
|
+
# pending), once a list note is in, at a turn's end (its moves) and after
|
|
60
|
+
# a rollback. A session with no list announces nothing. The caller
|
|
61
|
+
# (Engine#announce_objectives) holds the event lock, the one guard of
|
|
62
|
+
# the last view, announces the event and logs a failure: it only
|
|
63
|
+
# informs, never fails a turn.
|
|
64
|
+
# @param state_dir [String] the sessions dir
|
|
65
|
+
# @param running [Array<Hash>, nil] the running turn's conversation (Engine#live_conversation), nil between turns
|
|
66
|
+
# @return [Hash, nil] the :objectives_updated event to announce, nil when the view is the same
|
|
67
|
+
def announce(session, state_dir:, running:)
|
|
68
|
+
view = Samagotchi::Objectives.view(session.id, session.messages, state_dir: state_dir, running: running&.dup)
|
|
69
|
+
return if view.nil? || view == @announced
|
|
70
|
+
|
|
71
|
+
@announced = view
|
|
72
|
+
{ type: :objectives_updated, session_id: session.id, **view }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A change the model proposes (the objectives tool), kept until the turn
|
|
76
|
+
# is over, as TurnStages::ContextJudge's cards are: a card shown then is a quiet one, between turns, with its Apply kept
|
|
77
|
+
# over a restart (CardStore), not a running turn's that says it waits on the
|
|
78
|
+
# user. The same change twice in a turn is one card.
|
|
79
|
+
# @return [Boolean] false past Objectives::PROPOSALS_PER_TURN
|
|
80
|
+
def propose(proposal)
|
|
81
|
+
@proposals_mutex.synchronize do
|
|
82
|
+
next true if @proposals.any? { |p| p.line == proposal.line }
|
|
83
|
+
next false if @proposals.size >= Samagotchi::Objectives::PROPOSALS_PER_TURN
|
|
84
|
+
|
|
85
|
+
@proposals << proposal
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# However +turn+ ended (Engine#run_turn's ensure, after the turn flag is
|
|
91
|
+
# released): only a completed turn shows its proposal cards. A canceled,
|
|
92
|
+
# failed or rolled-back turn keeps none, and its proposals are dropped
|
|
93
|
+
# here so the next turn does not show them either: their Apply card
|
|
94
|
+
# would sit after a turn whose `objectives propose` call is not in the
|
|
95
|
+
# history (a rollback after Ctrl-C drops it). The caller
|
|
96
|
+
# (Engine#show_objective_proposals) shows the cards.
|
|
97
|
+
# @return [Array<Hash>] the completed turn's cards (Engine#show_card's
|
|
98
|
+
# arguments); none for any other ending
|
|
99
|
+
def finally(turn)
|
|
100
|
+
return proposal_cards if turn.completed?
|
|
101
|
+
|
|
102
|
+
discard_proposals(turn)
|
|
103
|
+
[]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# For specs: the audit's asks' thread for +kind+ (ObjectivesAudit's
|
|
107
|
+
# SideJudgments kinds), nil before any.
|
|
108
|
+
def audit_thread(kind) = @side_judgments.thread(kind)
|
|
109
|
+
|
|
110
|
+
# For specs: the proposals kept for the running turn (a copy).
|
|
111
|
+
def proposals = @proposals_mutex.synchronize { @proposals.dup }
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def proposal_cards
|
|
116
|
+
drain_proposals.map do |proposal|
|
|
117
|
+
id = "objectives-#{SecureRandom.hex(4)}"
|
|
118
|
+
{ source: Samagotchi::Objectives::SOURCE, id: id, title: proposal.title, body: proposal.body,
|
|
119
|
+
actions: [{ label: "Apply", command: proposal.action_line(id) },
|
|
120
|
+
{ label: "Dismiss", command: "/objectives --card #{id} dismiss" }] }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def discard_proposals(turn)
|
|
125
|
+
dropped = drain_proposals
|
|
126
|
+
Log.info(:turn, "objectives_proposals_dropped", count: dropped.size, outcome: turn.outcome.to_s) unless dropped.empty?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def drain_proposals
|
|
130
|
+
@proposals_mutex.synchronize { @proposals.slice!(0..) }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The audit for one call, over the values it is given: nothing of it
|
|
134
|
+
# is kept. Its client is asked for only once an active objective is
|
|
135
|
+
# there (ObjectivesAudit#ask), and with saving off nothing would come
|
|
136
|
+
# of an ask, so none is made.
|
|
137
|
+
def audit(state_dir, recorder)
|
|
138
|
+
ObjectivesAudit.new(
|
|
139
|
+
side: @side_judgments, state_dir: state_dir,
|
|
140
|
+
client_for: lambda do |judgment, session|
|
|
141
|
+
next nil unless recorder.saving?
|
|
142
|
+
|
|
143
|
+
recorder.wrap(SystemOne::Client.from_config(host_registry: @host_registry, mode: ObjectivesAudit::MODE, tag: :system_one),
|
|
144
|
+
judgment, session, mode: nil)
|
|
145
|
+
end
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|