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
|
@@ -8,6 +8,7 @@ require_relative "memory_paths"
|
|
|
8
8
|
require_relative "paths"
|
|
9
9
|
require_relative "tool_declarations"
|
|
10
10
|
require_relative "tools/memory"
|
|
11
|
+
require_relative "tools/tool_surface"
|
|
11
12
|
require_relative "muted_memories"
|
|
12
13
|
require_relative "model_notes"
|
|
13
14
|
require_relative "bundle_needs"
|
|
@@ -46,7 +47,8 @@ module Samagotchi
|
|
|
46
47
|
attr_reader :requested_memories
|
|
47
48
|
|
|
48
49
|
# @param profile [#call] → ModelProfile
|
|
49
|
-
# @param tools [#call] → Tools::Registry
|
|
50
|
+
# @param tools [#call] → Tools::Registry, nil (no tools): each build
|
|
51
|
+
# reads it as a Tools::ToolSurface under the build's layers
|
|
50
52
|
# @param session [#call] → Session, nil
|
|
51
53
|
# @param thinking [#call] → Symbol, the effective model's level (Thinking)
|
|
52
54
|
# @param model [#call] → ModelIdentity, nil (the section is left out)
|
|
@@ -77,6 +79,10 @@ module Samagotchi
|
|
|
77
79
|
# a new process. The files' text is read at each build.
|
|
78
80
|
@config_appended = Degrade.call(:prompt, "appended_list_unreadable", fallback: []) { ConfigFile.appended_prompt_files }
|
|
79
81
|
@cli_appended = Array(appended_files).map { |path| File.expand_path(path.to_s) }
|
|
82
|
+
# { [chat, thinking, layers] => Built }, swapped for a new one by #reset!
|
|
83
|
+
@cache = {}
|
|
84
|
+
# The Built #build returned last; nil before a build and after #reset!.
|
|
85
|
+
@last = nil
|
|
80
86
|
end
|
|
81
87
|
|
|
82
88
|
# @return [Array<String>] the names the session preloads, known before
|
|
@@ -85,37 +91,37 @@ module Samagotchi
|
|
|
85
91
|
@requested_memories.map { |raw| split_memory_scope(raw).last }.uniq
|
|
86
92
|
end
|
|
87
93
|
|
|
94
|
+
# A prompt #build made, with what it read while it did: the memory
|
|
95
|
+
# index sizes (#memory_index), the model notes, the files and the
|
|
96
|
+
# plugins' sections it holds, its stable part's length (chat) and the
|
|
97
|
+
# Tools::ToolSurface it declares (the tools, the not-loaded line, the
|
|
98
|
+
# kit's prompt text).
|
|
99
|
+
Built = Data.define(:prompt, :memory_index, :notes, :files, :sections, :stable_length, :surface)
|
|
100
|
+
|
|
88
101
|
# The full prompt: #base wrapped with the index and the rest. Built once
|
|
89
102
|
# per loop and level (until #reset!) so the prompt prefix, and the
|
|
90
103
|
# server's KV cache for it, stay stable.
|
|
104
|
+
#
|
|
105
|
+
# #reset! runs on other threads (a plugin's tools_changed! /
|
|
106
|
+
# prompt_changed!, a /stats re-resolving the profile) and, through the
|
|
107
|
+
# profile lookup, inside a build on this one. So a build reads @cache
|
|
108
|
+
# once and computes into locals: a reset meanwhile swaps in a new cache
|
|
109
|
+
# and this build's result goes into the dropped one, so it is returned
|
|
110
|
+
# (the same bytes as without the reset) but not kept, and the next
|
|
111
|
+
# build reads the sources again. No lock: the build reads memory files
|
|
112
|
+
# and calls the lookups, one of which takes ModelBinding's mutex, whose
|
|
113
|
+
# holder (a /stats thread) may itself call #reset!.
|
|
91
114
|
# @param chat [Boolean] for the chat loop
|
|
92
115
|
# @param thinking [Symbol, nil] the level (Thinking); nil: the effective model's
|
|
93
116
|
# @param layers [Array<Symbol>] the turn's LLM context layers: the
|
|
94
|
-
# native prompt declares the tools they add (Tools::
|
|
117
|
+
# native prompt declares the tools they add (Tools::ToolSurface#declared)
|
|
95
118
|
def build(chat: false, thinking: nil, layers: [])
|
|
96
119
|
key = [chat, thinking, layers]
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@
|
|
100
|
-
@
|
|
101
|
-
|
|
102
|
-
unless @built.key?(key)
|
|
103
|
-
@section_notes = nil
|
|
104
|
-
@section_files = []
|
|
105
|
-
@section_plugins = []
|
|
106
|
-
@built[key] = system_prompt_with_index(
|
|
107
|
-
assist_system_prompt(chat: chat, layers: layers), chat: chat, thinking: thinking, layers: layers
|
|
108
|
-
)
|
|
109
|
-
@index_sizes[key] = @measured_index
|
|
110
|
-
@notes_loaded[key] = @section_notes || []
|
|
111
|
-
@files_loaded[key] = @section_files.freeze
|
|
112
|
-
@sections_loaded[key] = @section_plugins.freeze
|
|
113
|
-
end
|
|
114
|
-
@memory_index = @index_sizes[key]
|
|
115
|
-
@prompt_notes = @notes_loaded[key]
|
|
116
|
-
@prompt_files = @files_loaded[key]
|
|
117
|
-
@prompt_sections = @sections_loaded[key]
|
|
118
|
-
@built[key]
|
|
120
|
+
cache = @cache
|
|
121
|
+
built = cache[key] || (cache[key] = build_prompt(chat: chat, thinking: thinking, layers: layers))
|
|
122
|
+
@memory_index = built.memory_index
|
|
123
|
+
@last = built
|
|
124
|
+
built.prompt
|
|
119
125
|
end
|
|
120
126
|
|
|
121
127
|
# The memory indexes the prompt #build returned last holds, per scope
|
|
@@ -130,7 +136,7 @@ module Samagotchi
|
|
|
130
136
|
# (Array<ModelNotes::Note>, [] with none), nil before a build and after
|
|
131
137
|
# #reset!. Read when the prompt is built, as the section is.
|
|
132
138
|
# @return [Array<ModelNotes::Note>, nil]
|
|
133
|
-
|
|
139
|
+
def prompt_notes = @last&.notes
|
|
134
140
|
|
|
135
141
|
# The files whose text the prompt #build returned last holds, as
|
|
136
142
|
# absolute paths in prompt order: the AGENTS.md chain, then the
|
|
@@ -138,26 +144,21 @@ module Samagotchi
|
|
|
138
144
|
# build and after #reset!. Read when the prompt is built, as the
|
|
139
145
|
# sections are.
|
|
140
146
|
# @return [Array<String>]
|
|
141
|
-
def prompt_files = @
|
|
147
|
+
def prompt_files = @last&.files || []
|
|
142
148
|
|
|
143
149
|
# The plugins' prompt sections the prompt #build returned last holds
|
|
144
150
|
# (Plugin::PromptSections::Built, prompt order), [] before a build and
|
|
145
151
|
# after #reset!.
|
|
146
152
|
# @return [Array<Plugin::PromptSections::Built>]
|
|
147
|
-
def prompt_sections = @
|
|
153
|
+
def prompt_sections = @last&.sections || []
|
|
148
154
|
|
|
149
155
|
# Drops the built prompts: the next #build reads the profile, tools,
|
|
150
156
|
# indexes and memories again (a model or profile switch, changed tools).
|
|
151
157
|
def reset!
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
@
|
|
155
|
-
@
|
|
156
|
-
@files_loaded = nil
|
|
157
|
-
@prompt_files = nil
|
|
158
|
-
@sections_loaded = nil
|
|
159
|
-
@prompt_sections = nil
|
|
160
|
-
@stable_lengths = nil
|
|
158
|
+
# Single assignments: a build in flight keeps the cache it read
|
|
159
|
+
# (#build), so it neither fails nor is kept.
|
|
160
|
+
@cache = {}
|
|
161
|
+
@last = nil
|
|
161
162
|
end
|
|
162
163
|
|
|
163
164
|
# The length of +prompt+'s stable part when it is a chat prompt #build
|
|
@@ -165,12 +166,18 @@ module Samagotchi
|
|
|
165
166
|
# a remote prompt cache can keep for every session (PromptCache). nil
|
|
166
167
|
# for any other text, or a prompt without those lines.
|
|
167
168
|
# @return [Integer, nil]
|
|
168
|
-
def stable_length(prompt)
|
|
169
|
+
def stable_length(prompt)
|
|
170
|
+
last = @last
|
|
171
|
+
return last.stable_length if last&.prompt == prompt
|
|
172
|
+
|
|
173
|
+
@cache.each_value.find { |built| built.prompt == prompt }&.stable_length
|
|
174
|
+
end
|
|
169
175
|
|
|
170
176
|
# The base prompt (specs, plugins' declarations), with Gemma's tool
|
|
171
177
|
# declarations at its end as #build has them.
|
|
172
178
|
def base(chat: false, layers: [])
|
|
173
|
-
|
|
179
|
+
surface = tool_surface(layers)
|
|
180
|
+
with_tail_declarations(assist_system_prompt(chat: chat, surface: surface), chat: chat, surface: surface)
|
|
174
181
|
end
|
|
175
182
|
|
|
176
183
|
# Names activated via preloaded --memory entries during prompt
|
|
@@ -182,6 +189,28 @@ module Samagotchi
|
|
|
182
189
|
|
|
183
190
|
private
|
|
184
191
|
|
|
192
|
+
# One #build's work: the prompt and what it read, in a Built. The
|
|
193
|
+
# @section_* and @measured_* scratch is this build's (the turn thread's)
|
|
194
|
+
# alone: #reset! never touches it.
|
|
195
|
+
def build_prompt(chat:, thinking:, layers:)
|
|
196
|
+
@section_notes = nil
|
|
197
|
+
@section_files = []
|
|
198
|
+
@section_plugins = []
|
|
199
|
+
@measured_stable_length = nil
|
|
200
|
+
surface = tool_surface(layers)
|
|
201
|
+
prompt = system_prompt_with_index(assist_system_prompt(chat: chat, surface: surface),
|
|
202
|
+
chat: chat, thinking: thinking, surface: surface)
|
|
203
|
+
Built.new(prompt: prompt, memory_index: @measured_index, notes: @section_notes || [],
|
|
204
|
+
files: @section_files.freeze, sections: @section_plugins.freeze, stable_length: @measured_stable_length,
|
|
205
|
+
surface: surface)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# The session's tools as a build under +layers+ declares them, read
|
|
209
|
+
# from the Registry now (one per build: the declarations, the
|
|
210
|
+
# not-loaded line and the kit's text agree).
|
|
211
|
+
# @return [Tools::ToolSurface]
|
|
212
|
+
def tool_surface(layers) = Tools::ToolSurface.for(@tools_lookup.call, layers: layers)
|
|
213
|
+
|
|
185
214
|
def profile = @profile_lookup.call
|
|
186
215
|
|
|
187
216
|
def turn_thinking = @thinking_lookup.call
|
|
@@ -192,8 +221,8 @@ module Samagotchi
|
|
|
192
221
|
|
|
193
222
|
# ── Tool declarations ──────────────────────────────────────────────────────
|
|
194
223
|
|
|
195
|
-
def tool_declarations(
|
|
196
|
-
schemas = ToolDeclarations.native_schemas(
|
|
224
|
+
def tool_declarations(surface)
|
|
225
|
+
schemas = ToolDeclarations.native_schemas(surface)
|
|
197
226
|
case profile.name
|
|
198
227
|
when "qwen36"
|
|
199
228
|
ToolDeclarations.qwen_declarations(schemas)
|
|
@@ -216,7 +245,7 @@ module Samagotchi
|
|
|
216
245
|
|
|
217
246
|
# @param chat [Boolean] for the chat loop: no tool declarations or call
|
|
218
247
|
# syntax (its tools go as schemas with each request)
|
|
219
|
-
def assist_system_prompt(chat: false
|
|
248
|
+
def assist_system_prompt(surface:, chat: false)
|
|
220
249
|
return chat_system_prompt if chat
|
|
221
250
|
|
|
222
251
|
hint = tool_call_hint
|
|
@@ -225,7 +254,7 @@ module Samagotchi
|
|
|
225
254
|
tools = if gemma_tail_declarations?
|
|
226
255
|
"Your tools are declared at the end of this system prompt."
|
|
227
256
|
else
|
|
228
|
-
"You have access to the following tools:\n\n#{tool_declarations(
|
|
257
|
+
"You have access to the following tools:\n\n#{tool_declarations(surface)}\n"
|
|
229
258
|
end
|
|
230
259
|
|
|
231
260
|
<<~SYS
|
|
@@ -316,7 +345,7 @@ module Samagotchi
|
|
|
316
345
|
# memory write changes them) after all of them; the model, the location
|
|
317
346
|
# and the session close the prompt (Gemma's tool declarations still
|
|
318
347
|
# follow them).
|
|
319
|
-
def system_prompt_with_index(base, chat: false, thinking: nil,
|
|
348
|
+
def system_prompt_with_index(base, chat: false, thinking: nil, surface: tool_surface([]))
|
|
320
349
|
thinking_token = chat ? "" : Thinking.native(thinking || turn_thinking, profile).system_token
|
|
321
350
|
indexes = { project: read_memory_index("project"), system: read_memory_index("system") }
|
|
322
351
|
@measured_index = measure_indexes(indexes)
|
|
@@ -324,15 +353,15 @@ module Samagotchi
|
|
|
324
353
|
"Project memories:\n#{indexes[:project]}",
|
|
325
354
|
"System memories:\n#{indexes[:system]}"
|
|
326
355
|
].join("\n\n")
|
|
327
|
-
stable = [thinking_token + base, not_loaded_line(
|
|
328
|
-
project_specific_description, appended_section, kit_prompt_section, plugin_prompt_section, memory_sections]
|
|
356
|
+
stable = [thinking_token + base, not_loaded_line(surface), rg_guidance, system_identity_section, model_notes_section, explicit_memory_section,
|
|
357
|
+
project_specific_description, appended_section, kit_prompt_section(surface), plugin_prompt_section, memory_sections]
|
|
329
358
|
stable_text = stable.compact.join("\n")
|
|
330
359
|
volatile = [participants_section, current_model, project_location, current_session].compact
|
|
331
|
-
return with_tail_declarations(stable_text, chat: chat,
|
|
360
|
+
return with_tail_declarations(stable_text, chat: chat, surface: surface) if volatile.empty?
|
|
332
361
|
|
|
333
362
|
prompt = "#{stable_text}\n#{volatile.join("\n")}"
|
|
334
|
-
|
|
335
|
-
with_tail_declarations(prompt, chat: chat,
|
|
363
|
+
@measured_stable_length = stable_text.length + 1 if chat
|
|
364
|
+
with_tail_declarations(prompt, chat: chat, surface: surface)
|
|
336
365
|
end
|
|
337
366
|
|
|
338
367
|
# The tools the session's toolkit leaves out, named once right after the
|
|
@@ -341,15 +370,12 @@ module Samagotchi
|
|
|
341
370
|
# the prompt is what it was before toolkits. The ones call_tool reaches
|
|
342
371
|
# and the ones the toolkit removes (only the user brings those back)
|
|
343
372
|
# get a sentence each; with call_tool not declared, one list.
|
|
344
|
-
def not_loaded_line(
|
|
345
|
-
|
|
346
|
-
return nil unless registry.respond_to?(:left_out_names)
|
|
347
|
-
|
|
348
|
-
names = registry.left_out_names(layers: layers)
|
|
373
|
+
def not_loaded_line(surface)
|
|
374
|
+
names = surface.left_out.keys
|
|
349
375
|
return nil if names.empty?
|
|
350
376
|
|
|
351
|
-
toolkit = "(toolkit #{
|
|
352
|
-
reachable =
|
|
377
|
+
toolkit = "(toolkit #{surface.toolkit&.name})"
|
|
378
|
+
reachable = surface.index_only.map(&:name)
|
|
353
379
|
if reachable.empty?
|
|
354
380
|
return "Not loaded in this session #{toolkit}: #{names.join(", ")}. Only the user can load one (/toolkit add <name>)."
|
|
355
381
|
end
|
|
@@ -371,10 +397,7 @@ module Samagotchi
|
|
|
371
397
|
# sessions on that kit share it, and a memory write doesn't move it.
|
|
372
398
|
# nil on a kit without one and on full: the prompt is what it was
|
|
373
399
|
# before kit prompts.
|
|
374
|
-
def kit_prompt_section
|
|
375
|
-
registry = @tools_lookup.call
|
|
376
|
-
registry.respond_to?(:toolkit) ? registry.toolkit&.prompt : nil
|
|
377
|
-
end
|
|
400
|
+
def kit_prompt_section(surface) = surface.toolkit&.prompt
|
|
378
401
|
|
|
379
402
|
# The plugins' prompt sections (chi.prompt_section), right after the
|
|
380
403
|
# kit prompt and before the memory indexes: a plugin's text depends on
|
|
@@ -390,8 +413,8 @@ module Samagotchi
|
|
|
390
413
|
end
|
|
391
414
|
end
|
|
392
415
|
|
|
393
|
-
def with_tail_declarations(prompt, chat:,
|
|
394
|
-
chat || !gemma_tail_declarations? ? prompt : prompt.rstrip + tool_declarations(
|
|
416
|
+
def with_tail_declarations(prompt, chat:, surface:)
|
|
417
|
+
chat || !gemma_tail_declarations? ? prompt : prompt.rstrip + tool_declarations(surface)
|
|
395
418
|
end
|
|
396
419
|
|
|
397
420
|
def gemma_tail_declarations? = profile.name == "gemma4"
|
|
@@ -60,15 +60,6 @@ module Samagotchi
|
|
|
60
60
|
@screen.commit(text)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
# Draw on +surface+ from now on (no caller since the REPL went): what the slot shows
|
|
64
|
-
# moves there.
|
|
65
|
-
def surface=(surface)
|
|
66
|
-
@lock.synchronize do
|
|
67
|
-
@screen = surface
|
|
68
|
-
redraw_status if status_text
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
63
|
# A new turn: its tally starts over too.
|
|
73
64
|
def reset_turn_feedback
|
|
74
65
|
@lock.synchronize do
|
|
@@ -5,6 +5,7 @@ require "fileutils"
|
|
|
5
5
|
require "reline"
|
|
6
6
|
|
|
7
7
|
require_relative "../config"
|
|
8
|
+
require_relative "../degrade"
|
|
8
9
|
require_relative "../memory_paths"
|
|
9
10
|
require_relative "../prompt_history"
|
|
10
11
|
require_relative "../tools/memory"
|
|
@@ -90,20 +91,18 @@ module Samagotchi
|
|
|
90
91
|
fragment = slash_fragment.to_s.strip
|
|
91
92
|
return [] unless fragment.start_with?("/")
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
# Only at the line's start: "/x" mid-line is a path, not a command.
|
|
95
|
+
Degrade.call(:attached, "completion_line_unreadable") do
|
|
94
96
|
buf = Reline.line_buffer.to_s
|
|
95
|
-
if !buf.empty? && !buf.lstrip.start_with?("/")
|
|
96
|
-
return []
|
|
97
|
-
end
|
|
98
|
-
rescue StandardError
|
|
99
|
-
nil
|
|
97
|
+
return [] if !buf.empty? && !buf.lstrip.start_with?("/")
|
|
100
98
|
end
|
|
101
99
|
|
|
102
100
|
lowered = fragment.downcase
|
|
103
101
|
return slash_commands.dup if lowered == "/"
|
|
104
102
|
|
|
105
103
|
slash_commands.select { |cmd| cmd.start_with?(lowered) }
|
|
106
|
-
rescue StandardError
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
Degrade.note(:attached, "slash_completion_failed", e)
|
|
107
106
|
[]
|
|
108
107
|
end
|
|
109
108
|
|
|
@@ -134,7 +133,8 @@ module Samagotchi
|
|
|
134
133
|
relative_path = "#{relative_path}/" if File.directory?(absolute_path)
|
|
135
134
|
"#{AT_PATH_COMPLETION_PREFIX}#{relative_path}"
|
|
136
135
|
end
|
|
137
|
-
rescue StandardError
|
|
136
|
+
rescue StandardError => e
|
|
137
|
+
Degrade.note(:attached, "path_completion_failed", e)
|
|
138
138
|
[]
|
|
139
139
|
end
|
|
140
140
|
|
|
@@ -190,7 +190,8 @@ module Samagotchi
|
|
|
190
190
|
yield scope, name
|
|
191
191
|
end
|
|
192
192
|
end
|
|
193
|
-
rescue StandardError
|
|
193
|
+
rescue StandardError => e
|
|
194
|
+
Degrade.note(:attached, "memory_completion_failed", e)
|
|
194
195
|
[]
|
|
195
196
|
end
|
|
196
197
|
|
|
@@ -207,8 +208,8 @@ module Samagotchi
|
|
|
207
208
|
@history_signature = PromptHistory.signature
|
|
208
209
|
@history_seen = PromptHistory.entries
|
|
209
210
|
@history_seen.last(PromptHistory::LIMIT).each { |entry| Reline::HISTORY << entry }
|
|
210
|
-
rescue StandardError
|
|
211
|
-
|
|
211
|
+
rescue StandardError => e
|
|
212
|
+
Degrade.note(:attached, "history_load_failed", e)
|
|
212
213
|
end
|
|
213
214
|
|
|
214
215
|
# A scratch session keeps nothing, its typed lines neither.
|
|
@@ -223,8 +224,8 @@ module Samagotchi
|
|
|
223
224
|
history_lock.synchronize { forget_own_line(own) } if own
|
|
224
225
|
raise
|
|
225
226
|
end
|
|
226
|
-
rescue StandardError
|
|
227
|
-
|
|
227
|
+
rescue StandardError => e
|
|
228
|
+
Degrade.note(:attached, "history_append_failed", e)
|
|
228
229
|
end
|
|
229
230
|
|
|
230
231
|
# Before a main-prompt read, and on the first ↑ of a walk at an open
|
|
@@ -255,8 +256,8 @@ module Samagotchi
|
|
|
255
256
|
@history_signature = signature
|
|
256
257
|
@history_seen = entries
|
|
257
258
|
history_lock.synchronize { add_others_lines(fresh) }
|
|
258
|
-
rescue StandardError
|
|
259
|
-
|
|
259
|
+
rescue StandardError => e
|
|
260
|
+
Degrade.note(:attached, "history_pickup_failed", e)
|
|
260
261
|
end
|
|
261
262
|
end
|
|
262
263
|
|
|
@@ -77,8 +77,11 @@ module Samagotchi
|
|
|
77
77
|
Log.session_id = @link.client.session_id
|
|
78
78
|
Log.info(:attached, "joined", session: @link.client.session_id, messages: exchange.size,
|
|
79
79
|
notes: messages.count { |m| ContextNote.note?(m) })
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
# The last turn's own prompt (Steer.turn_prompt?, as the page's
|
|
81
|
+
# turns): not Qwen's recovery line, nor input merged into the turn.
|
|
82
|
+
last_user = exchange.rindex { |m| Steer.turn_prompt?(m) }
|
|
83
|
+
from = last_user && messages.index { |m| m.equal?(exchange[last_user]) }
|
|
84
|
+
render_join_notes(messages, from)
|
|
82
85
|
return unless last_user
|
|
83
86
|
|
|
84
87
|
@screen.commit(join_prompt_line(exchange[last_user]))
|
|
@@ -86,7 +89,6 @@ module Samagotchi
|
|
|
86
89
|
unless ImageRef.placeholder_only?(exchange[last_user][:content])
|
|
87
90
|
Array(exchange[last_user][:images]).each { |ref| @screen.commit(format_image_line(ref)) }
|
|
88
91
|
end
|
|
89
|
-
from = messages.index { |m| m.equal?(exchange[last_user]) }
|
|
90
92
|
render_join_steps(messages, from, durations: join_tool_records(exchange[last_user]))
|
|
91
93
|
record = @link.saved_turn_record(exchange[last_user][:turn_id] || exchange[last_user]["turn_id"])
|
|
92
94
|
failed = join_failed_record(record)
|
|
@@ -114,19 +116,21 @@ module Samagotchi
|
|
|
114
116
|
.find { |text| !text.empty? }
|
|
115
117
|
@screen.commit(last_lines(answer)) if answer
|
|
116
118
|
end
|
|
117
|
-
render_join_notes(messages, after_exchange: true)
|
|
119
|
+
render_join_notes(messages, from, after_exchange: true)
|
|
118
120
|
end
|
|
119
121
|
|
|
120
122
|
# What the last exchange did, as the live turn drew it: each saved
|
|
121
123
|
# call's tool row (action, params, status from its result, and its
|
|
122
124
|
# duration from +durations+, the turn's saved tool records) and a
|
|
123
|
-
# plugin's steer
|
|
124
|
-
# ticker's).
|
|
125
|
+
# plugin's steer and the user's input merged into the turn where they
|
|
126
|
+
# came. The step texts stay out, as live (the ticker's).
|
|
125
127
|
def render_join_steps(messages, from, durations: [])
|
|
126
128
|
records = durations.dup
|
|
127
129
|
messages.each_with_index.drop(from + 1).each do |m, i|
|
|
128
130
|
if Steer.steer?(m)
|
|
129
131
|
@screen.commit(format_steer_line(source: m[:source], text: m[:content]))
|
|
132
|
+
elsif Steer.input?(m) && !Steer.turn_prompt?(m)
|
|
133
|
+
@screen.commit("input> #{m[:content]}")
|
|
130
134
|
elsif %w[model assistant].include?(m[:role].to_s)
|
|
131
135
|
responses = messages.drop(i + 1).take_while { |r| r[:role].to_s == "tool_response" }
|
|
132
136
|
Array(MessageParts.for_message(m, responses)&.dig(:tools)).each do |part|
|
|
@@ -209,13 +213,13 @@ module Samagotchi
|
|
|
209
213
|
format_tool_guardrail_suffix({ decided_by: "system one", p: saved["p"] || saved[:p] })
|
|
210
214
|
end
|
|
211
215
|
|
|
212
|
-
# The context notes that came since the last prompt (
|
|
213
|
-
# session with none), after the
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return if after_exchange != !
|
|
216
|
+
# The context notes that came since the last prompt (+from+, its index
|
|
217
|
+
# in +messages+; all of them in a session with none), after the
|
|
218
|
+
# exchange the header shows.
|
|
219
|
+
def render_join_notes(messages, from, after_exchange: false)
|
|
220
|
+
return if after_exchange != !from.nil?
|
|
217
221
|
|
|
218
|
-
messages[(
|
|
222
|
+
messages[(from ? from + 1 : 0)..].select { |m| ContextNote.note?(m) }.each do |m|
|
|
219
223
|
@screen.commit(context_note_line(ContextNote.label_of(m), ContextNote.text_of(m)))
|
|
220
224
|
end
|
|
221
225
|
end
|
|
@@ -44,11 +44,6 @@ module Samagotchi
|
|
|
44
44
|
# @return [#call, nil]
|
|
45
45
|
attr_accessor :interrupt_handler
|
|
46
46
|
|
|
47
|
-
# Called with no arguments for each key a read takes (typing is
|
|
48
|
-
# activity for an idle clock; nobody sets it now).
|
|
49
|
-
# @return [#call, nil]
|
|
50
|
-
attr_accessor :key_handler
|
|
51
|
-
|
|
52
47
|
# @return [Boolean] a Reline read is open (it owns stdin)
|
|
53
48
|
def reading? = @reading == true
|
|
54
49
|
|
|
@@ -150,11 +145,6 @@ module Samagotchi
|
|
|
150
145
|
@rendered_screen.base_y = 0 if RelineSeam.screen
|
|
151
146
|
end
|
|
152
147
|
|
|
153
|
-
def update(key)
|
|
154
|
-
RelineSeam.key_handler&.call
|
|
155
|
-
super
|
|
156
|
-
end
|
|
157
|
-
|
|
158
148
|
def screen_height
|
|
159
149
|
screen = RelineSeam.screen
|
|
160
150
|
screen ? [super, screen.editor_budget].min : super
|
|
@@ -54,13 +54,6 @@ module Samagotchi
|
|
|
54
54
|
@shown = nil
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
# The surface it draws on (no caller since the REPL went): the row
|
|
58
|
-
# is drawn there at the next change.
|
|
59
|
-
def surface=(surface)
|
|
60
|
-
@surface = surface
|
|
61
|
-
@shown = nil
|
|
62
|
-
end
|
|
63
|
-
|
|
64
57
|
def [](field) = @values[field]
|
|
65
58
|
|
|
66
59
|
# Change some fields; the row is redrawn when its text changed.
|
|
@@ -94,6 +94,13 @@ module Samagotchi
|
|
|
94
94
|
"Samagotchi::AnswerRefs.new" => "spec: after_turn's refs: helper, as chi's fire site builds it",
|
|
95
95
|
"Samagotchi::Log" => "spec: stubs Log to check what ctx.log recorded: a kit log recorder could replace it"
|
|
96
96
|
},
|
|
97
|
+
"inline-widgets" => {
|
|
98
|
+
'require "samagotchi/inline_widgets"' => "spec: for InlineWidgets",
|
|
99
|
+
"Samagotchi::InlineWidgets.parse" => "spec: each example the sections teach parses strictly, as chi web " \
|
|
100
|
+
"parses a block (the drawing is core's, so is the parse)",
|
|
101
|
+
"Samagotchi::Plugin::PromptSections::MAX" => "spec: the prompt-section limit a section over is dropped at: " \
|
|
102
|
+
"could be a stable constant"
|
|
103
|
+
},
|
|
97
104
|
"loop-guard" => {
|
|
98
105
|
'require "samagotchi/llm/native_tool_normalizer"' => "spec: for NativeToolNormalizer",
|
|
99
106
|
"Samagotchi::LLM::NativeToolNormalizer.normalize" => "spec: replays recorded calls in the shape chi's parser gives",
|
|
@@ -11,6 +11,7 @@ require_relative "../tools/tool_index"
|
|
|
11
11
|
require_relative "../peer_sources"
|
|
12
12
|
require_relative "../toolkit"
|
|
13
13
|
require_relative "../guardrails/gate"
|
|
14
|
+
require_relative "../turn_context"
|
|
14
15
|
require_relative "../memory_bundle/manifest"
|
|
15
16
|
require_relative "../memory_bundle/source"
|
|
16
17
|
require_relative "../session_setup"
|
|
@@ -239,8 +240,9 @@ module Samagotchi
|
|
|
239
240
|
dispatched = nil
|
|
240
241
|
capture = Hooks::Registry.new
|
|
241
242
|
capture.register(:before_tool_call) { |event| dispatched = event.dup }
|
|
242
|
-
gate = Guardrails::Gate.new(
|
|
243
|
-
gate.evaluate(call, iteration: iteration, params: params,
|
|
243
|
+
gate = Guardrails::Gate.new(hooks: capture, tools: tools, context_for: ->(_turn) { context })
|
|
244
|
+
gate.evaluate(call, context: TurnContext.none, iteration: iteration, params: params,
|
|
245
|
+
verdict: verdict || Verdict.new(call: call))
|
|
244
246
|
raise ArgumentError, "guardrails did not dispatch a before_tool_call event for #{call[:name].inspect}" unless dispatched
|
|
245
247
|
|
|
246
248
|
dispatched.slice(:type, :iteration, :call, :params, :blocked, :block_reason, :guardrail, :context, :targets)
|
|
@@ -256,11 +258,11 @@ module Samagotchi
|
|
|
256
258
|
def tool_call(call, params: nil, cwd: nil, context: nil, verdict: nil, iteration: 1)
|
|
257
259
|
context ||= Guardrails::Context.new(cwd: cwd || @recorder[:cwd], session_id: @recorder[:session_id],
|
|
258
260
|
interface: @recorder[:frontend])
|
|
259
|
-
gate = Guardrails::Gate.new(
|
|
260
|
-
|
|
261
|
+
gate = Guardrails::Gate.new(hooks: hooks, tools: tools, context_for: ->(_turn) { context },
|
|
262
|
+
checks_for: ->(_turn) { [] })
|
|
261
263
|
verdict ||= Verdict.new(call: call)
|
|
262
264
|
params ||= "#{call[:name]} #{call[:content] || call[:path]}".strip
|
|
263
|
-
failing { gate.evaluate(call, iteration: iteration, params: params, verdict: verdict) }
|
|
265
|
+
failing { gate.evaluate(call, context: TurnContext.none, iteration: iteration, params: params, verdict: verdict) }
|
|
264
266
|
end
|
|
265
267
|
|
|
266
268
|
# Run a command line ("/name args") as chi does: the first matching
|
|
@@ -31,7 +31,7 @@ module Samagotchi
|
|
|
31
31
|
# What a bundle registered (#registered): commands, tools (names),
|
|
32
32
|
# hooks per event, inits (labels), toolkits, tool sources and peer
|
|
33
33
|
# sources (names).
|
|
34
|
-
Registered = Data.define(:commands, :tools, :hooks, :inits, :toolkits, :tool_sources, :peer_sources)
|
|
34
|
+
Registered = Data.define(:commands, :tools, :hooks, :inits, :toolkits, :tool_sources, :peer_sources, :prompt_sections)
|
|
35
35
|
# A command: its mid_turn policy and its selection-bar labels (chi.annotation).
|
|
36
36
|
Command = Data.define(:name, :mid_turn, :labels)
|
|
37
37
|
# A hook on an event: the bundle, the hook (its file, or the plugin's) and its priority.
|
|
@@ -98,7 +98,8 @@ module Samagotchi
|
|
|
98
98
|
inits: inits.map { |init| init[:label] },
|
|
99
99
|
toolkits: Array(registries.toolkits).map(&:name),
|
|
100
100
|
tool_sources: registries.tool_index.sources.map(&:label),
|
|
101
|
-
peer_sources: registries.peer_sources.names
|
|
101
|
+
peer_sources: registries.peer_sources.names,
|
|
102
|
+
prompt_sections: registries.prompt_sections.names(name)
|
|
102
103
|
)
|
|
103
104
|
end
|
|
104
105
|
|
|
@@ -58,11 +58,11 @@ module Samagotchi
|
|
|
58
58
|
# @param registry [Tools::Registry, nil] the session's tools
|
|
59
59
|
# @param cwd [String, nil] what a file tool's title is relative to (the
|
|
60
60
|
# worker runs in its session's working directory)
|
|
61
|
-
def tool_activity_event(tool_name, call, result, registry: nil, cwd: Dir.pwd)
|
|
61
|
+
def tool_activity_event(tool_name, call, result, registry: nil, cwd: Dir.pwd, surface: nil)
|
|
62
62
|
event = {
|
|
63
63
|
action: tool_activity_action(tool_name, registry: registry, call: call),
|
|
64
64
|
tool: tool_name,
|
|
65
|
-
params: tool_activity_params(tool_name, call, registry: registry),
|
|
65
|
+
params: tool_activity_params(tool_name, call, registry: registry, surface: surface),
|
|
66
66
|
status: tool_activity_status(result, tool_name, command: call.is_a?(Hash) ? call[:content] : nil)
|
|
67
67
|
}
|
|
68
68
|
# A grep that found nothing: ok, and the web row says "no match".
|
|
@@ -359,7 +359,9 @@ module Samagotchi
|
|
|
359
359
|
name == "git" && args.first == "diff" && args.intersect?(%w[--quiet --exit-code])
|
|
360
360
|
end
|
|
361
361
|
|
|
362
|
-
|
|
362
|
+
# +surface+: the turn's Tools::ToolSurface, for a preview that reads it
|
|
363
|
+
# (call_tool's); nil outside a turn.
|
|
364
|
+
def tool_activity_params(tool_name, call, registry: nil, surface: nil)
|
|
363
365
|
case tool_name
|
|
364
366
|
when Tools::Execute::NAME
|
|
365
367
|
"command=#{preview_tool_param(call[:content])}"
|
|
@@ -453,7 +455,7 @@ module Samagotchi
|
|
|
453
455
|
parts << "options=#{preview_tool_param(Array(opts).join(","))}" if opts && !Array(opts).empty?
|
|
454
456
|
parts.join(" ")
|
|
455
457
|
else
|
|
456
|
-
registry_params(registry_entry(registry, tool_name), call)
|
|
458
|
+
registry_params(registry_entry(registry, tool_name), call, surface)
|
|
457
459
|
end
|
|
458
460
|
end
|
|
459
461
|
|
|
@@ -472,13 +474,13 @@ module Samagotchi
|
|
|
472
474
|
# the web's reload also has); nil for a tool the registry doesn't know
|
|
473
475
|
# that has no args:, and for a built-in without its own words above
|
|
474
476
|
# (the reminder tools).
|
|
475
|
-
def registry_params(entry, call)
|
|
477
|
+
def registry_params(entry, call, surface = nil)
|
|
476
478
|
return nil if entry&.core?
|
|
477
479
|
return nil if entry.nil? && !call[:args].is_a?(Hash)
|
|
478
480
|
|
|
479
481
|
if entry&.preview
|
|
480
482
|
begin
|
|
481
|
-
return entry.
|
|
483
|
+
return entry.preview_of(call, surface)
|
|
482
484
|
rescue StandardError => e
|
|
483
485
|
Log.warn(:plugins, "plugin_preview_failed", tool: entry.name, error: e.class.name,
|
|
484
486
|
msg: "#{entry.name} preview failed: #{e.message}")
|
|
@@ -635,10 +635,10 @@ module Samagotchi
|
|
|
635
635
|
|
|
636
636
|
# The schemas the native (Gemma, Qwen) prompts declare: the built-ins
|
|
637
637
|
# as they are, and each plugin tool's flattened (.flat_schema).
|
|
638
|
-
# @param
|
|
639
|
-
#
|
|
640
|
-
def native_schemas(
|
|
641
|
-
|
|
638
|
+
# @param surface [Tools::ToolSurface] the tools a turn (or a build
|
|
639
|
+
# under its layers) declares (ToolSurface#declared)
|
|
640
|
+
def native_schemas(surface)
|
|
641
|
+
surface.declared.map { |entry| entry.core? ? entry.schema : flat_schema(entry.schema) }
|
|
642
642
|
end
|
|
643
643
|
|
|
644
644
|
# A plugin tool's schema in the shape the built-ins have, which is all
|
|
@@ -34,14 +34,15 @@ module Samagotchi
|
|
|
34
34
|
# own go to +on_stream_event+. An Interrupt (Ctrl-C) mid-batch yields an
|
|
35
35
|
# #interrupted run for every call left (the one it cut short too), so
|
|
36
36
|
# the loop's conversation answers each call, then goes on up: the turn
|
|
37
|
-
# keeps its progress (Engine#ctrl_c_messages).
|
|
37
|
+
# keeps its progress (Engine#ctrl_c_messages). +context+: the calls'
|
|
38
|
+
# turn (TurnContext), each run's (ToolRunner#run).
|
|
38
39
|
# @return [Array<ToolRun>] the runs
|
|
39
|
-
def run_batch(runner, calls, iteration:, emit:, on_stream_event:, cap:)
|
|
40
|
+
def run_batch(runner, calls, context:, iteration:, emit:, on_stream_event:, cap:)
|
|
40
41
|
# Before anything can be interrupted: the rescue counts it.
|
|
41
42
|
runs = []
|
|
42
43
|
emit.call({ type: :tool_dispatch_started, iteration: iteration, call_count: calls.length })
|
|
43
44
|
calls.each_with_index do |call, index|
|
|
44
|
-
runs << runner.run(call, iteration: iteration, call_index: index + 1, call_count: calls.length,
|
|
45
|
+
runs << runner.run(call, context: context, iteration: iteration, call_index: index + 1, call_count: calls.length,
|
|
45
46
|
on_stream_event: on_stream_event, max_tool_output_chars: cap)
|
|
46
47
|
yield runs.last, index if block_given?
|
|
47
48
|
end
|