samagotchi 0.152.0 → 0.154.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 +45 -1
- data/README.md +2 -3
- data/bin/chi +23 -24
- data/docs/architecture.md +30 -33
- data/docs/broadcast.md +5 -5
- data/docs/bundle-authoring.md +1 -1
- data/docs/bundles.md +93 -12
- data/docs/cli.md +83 -99
- data/docs/configuration.md +46 -40
- data/docs/context.md +3 -3
- data/docs/desktop.md +1 -1
- data/docs/guardrails.md +15 -15
- data/docs/hooks.md +13 -12
- data/docs/internals/anthropic.md +3 -3
- data/docs/internals/attached-context.md +5 -2
- data/docs/internals/background-tasks.md +3 -4
- data/docs/internals/block-renderers.md +4 -0
- data/docs/internals/bridge.md +3 -3
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +42 -12
- data/docs/internals/objectives.md +6 -5
- data/docs/internals/participants.md +8 -8
- data/docs/internals/prompt-caching.md +8 -4
- data/docs/internals/remote-sessions.md +7 -7
- data/docs/internals/web-frontend.md +20 -17
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +2 -2
- data/docs/plugins.md +52 -43
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +25 -25
- data/docs/sub-agent.md +2 -2
- data/docs/system-one.md +1 -1
- data/docs/testing.md +18 -8
- data/lib/samagotchi/answer_text.rb +34 -0
- data/lib/samagotchi/bridge.rb +43 -32
- data/lib/samagotchi/bridge_client.rb +10 -9
- data/lib/samagotchi/broadcast/recipients.rb +2 -2
- data/lib/samagotchi/broadcast/triage.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +5 -5
- data/lib/samagotchi/bundle_command/try/command.rb +1 -1
- data/lib/samagotchi/bundle_command/try/run.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
- data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/bundles/web-search/manifest.yml +13 -0
- data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
- data/lib/samagotchi/child_move.rb +3 -3
- data/lib/samagotchi/commands/registry.rb +6 -6
- data/lib/samagotchi/composer_route.rb +4 -5
- data/lib/samagotchi/config.rb +14 -12
- data/lib/samagotchi/context_candidate_choice.rb +1 -1
- data/lib/samagotchi/context_candidates.rb +4 -3
- data/lib/samagotchi/continue_offer.rb +1 -1
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/degrade.rb +75 -0
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
- data/lib/samagotchi/engine/cards.rb +87 -0
- data/lib/samagotchi/engine/context_judge.rb +138 -0
- data/lib/samagotchi/engine/events.rb +131 -0
- data/lib/samagotchi/engine/guardrails.rb +87 -0
- data/lib/samagotchi/engine/hooks.rb +119 -0
- data/lib/samagotchi/engine/messages.rb +124 -0
- data/lib/samagotchi/engine/model_settings.rb +127 -0
- data/lib/samagotchi/engine/models.rb +261 -0
- data/lib/samagotchi/engine/objectives.rb +116 -0
- data/lib/samagotchi/engine/participants.rb +86 -0
- data/lib/samagotchi/engine/plugins.rb +208 -0
- data/lib/samagotchi/engine/questions.rb +98 -0
- data/lib/samagotchi/engine/recap.rb +130 -0
- data/lib/samagotchi/engine/session.rb +131 -0
- data/lib/samagotchi/engine/side_requests.rb +135 -0
- data/lib/samagotchi/engine/steering.rb +134 -0
- data/lib/samagotchi/engine/toolkit.rb +269 -0
- data/lib/samagotchi/engine/turn_end.rb +180 -0
- data/lib/samagotchi/engine/turn_helpers.rb +160 -0
- data/lib/samagotchi/engine/verify.rb +153 -0
- data/lib/samagotchi/engine.rb +63 -2642
- data/lib/samagotchi/grants.rb +575 -0
- data/lib/samagotchi/guardrail_wiring.rb +2 -2
- data/lib/samagotchi/guardrails/context.rb +2 -3
- data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
- data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
- data/lib/samagotchi/guardrails/review_boundary.rb +4 -3
- data/lib/samagotchi/hook_routes.rb +3 -3
- data/lib/samagotchi/idle_recap.rb +4 -5
- data/lib/samagotchi/idle_reminders.rb +3 -4
- data/lib/samagotchi/iteration_limit.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +10 -11
- data/lib/samagotchi/launch_mode.rb +13 -32
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/llm_context_command.rb +3 -2
- data/lib/samagotchi/llm_context_stale.rb +2 -2
- data/lib/samagotchi/log.rb +7 -7
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/log_path.rb +1 -1
- data/lib/samagotchi/message_parts.rb +3 -2
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +18 -8
- data/lib/samagotchi/objectives.rb +24 -3
- data/lib/samagotchi/objectives_check.rb +8 -27
- data/lib/samagotchi/objectives_command.rb +4 -69
- data/lib/samagotchi/objectives_edit.rb +87 -0
- data/lib/samagotchi/output_formatter.rb +7 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/parent_answers_command.rb +5 -4
- data/lib/samagotchi/participant_exposure.rb +42 -0
- data/lib/samagotchi/participant_mode.rb +1 -1
- data/lib/samagotchi/participant_name.rb +1 -1
- data/lib/samagotchi/participants.rb +17 -17
- data/lib/samagotchi/participants_watch.rb +1 -1
- data/lib/samagotchi/pending_input_queue.rb +2 -2
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/plugin/context.rb +21 -7
- data/lib/samagotchi/plugin/peer.rb +2 -1
- data/lib/samagotchi/plugin/sessions.rb +6 -4
- data/lib/samagotchi/plugin_tasks.rb +1 -2
- data/lib/samagotchi/private_files.rb +29 -0
- data/lib/samagotchi/project_env.rb +2 -2
- data/lib/samagotchi/question_desk.rb +8 -7
- data/lib/samagotchi/recap_command.rb +2 -1
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/recap_store.rb +1 -1
- data/lib/samagotchi/redact.rb +86 -0
- data/lib/samagotchi/reminder_queue.rb +2 -2
- data/lib/samagotchi/remote_attach_command.rb +1 -1
- data/lib/samagotchi/reply_wait/poll.rb +2 -2
- data/lib/samagotchi/review_start.rb +1 -1
- data/lib/samagotchi/run_options.rb +6 -6
- data/lib/samagotchi/self_report.rb +42 -39
- data/lib/samagotchi/send_command/busy.rb +1 -1
- data/lib/samagotchi/send_command.rb +4 -1
- data/lib/samagotchi/session.rb +11 -2
- data/lib/samagotchi/session_access/contract.rb +3 -3
- data/lib/samagotchi/session_access/local.rb +9 -9
- data/lib/samagotchi/session_access/remote.rb +17 -7
- data/lib/samagotchi/session_access.rb +4 -4
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +8 -6
- data/lib/samagotchi/session_commands.rb +45 -48
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_delete_command.rb +4 -4
- data/lib/samagotchi/session_manager.rb +59 -36
- data/lib/samagotchi/session_metrics.rb +2 -2
- data/lib/samagotchi/session_model_settings.rb +3 -2
- data/lib/samagotchi/session_participants.rb +1 -1
- data/lib/samagotchi/session_retention.rb +3 -3
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +10 -10
- data/lib/samagotchi/steer.rb +1 -1
- data/lib/samagotchi/system_prompt.rb +102 -109
- data/lib/samagotchi/task_watch.rb +1 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
- data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
- data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
- data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
- data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
- data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
- data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
- data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
- data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
- data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
- data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
- data/lib/samagotchi/terminal_ui.rb +142 -1387
- data/lib/samagotchi/title_command.rb +2 -2
- data/lib/samagotchi/tool_runner.rb +14 -16
- data/lib/samagotchi/toolkit.rb +8 -5
- data/lib/samagotchi/toolkit_binding.rb +30 -5
- data/lib/samagotchi/toolkit_command.rb +2 -1
- data/lib/samagotchi/toolkit_report_command.rb +9 -5
- data/lib/samagotchi/toolkit_suggest.rb +12 -6
- data/lib/samagotchi/tools/builtin_calls.rb +14 -10
- data/lib/samagotchi/tools/deferred_tools.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +3 -1
- data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
- data/lib/samagotchi/tools/delegate_wait.rb +1 -1
- data/lib/samagotchi/tools/objectives_tool.rb +2 -2
- data/lib/samagotchi/tools/registry.rb +44 -20
- data/lib/samagotchi/tools/send_note.rb +2 -1
- data/lib/samagotchi/tools/tool_index.rb +27 -21
- data/lib/samagotchi/turn_endings.rb +2 -2
- data/lib/samagotchi/turn_flow.rb +4 -5
- data/lib/samagotchi/update_hint.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +39 -50
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_review.rb +5 -5
- data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
- data/lib/samagotchi/web/participant_view.rb +2 -82
- data/lib/samagotchi/web/participants_admin.rb +1 -1
- data/lib/samagotchi/web/public/app.js +61 -357
- data/lib/samagotchi/web/public/block_host.js +35 -8
- data/lib/samagotchi/web/public/block_renderers.css +9 -5
- data/lib/samagotchi/web/public/chains.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer_mode.js +110 -0
- data/lib/samagotchi/web/public/format.js +3 -3
- data/lib/samagotchi/web/public/owner_claim.js +81 -0
- data/lib/samagotchi/web/public/owner_verify.js +112 -0
- data/lib/samagotchi/web/public/session_fields.js +3 -3
- data/lib/samagotchi/web/public/strip_grip.js +3 -3
- data/lib/samagotchi/web/public/strip_hidden.js +65 -0
- data/lib/samagotchi/web/public/version_notices.js +132 -0
- data/lib/samagotchi/web/push/store.rb +1 -1
- data/lib/samagotchi/web/session_access_routes.rb +5 -4
- data/lib/samagotchi/web/session_control.rb +12 -8
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +2 -2
- data/lib/samagotchi/web/session_show.rb +2 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +106 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- metadata +36 -6
- data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
- data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
- data/lib/samagotchi/web/grants.rb +0 -575
- data/lib/samagotchi/web/participant_exposure.rb +0 -44
- data/lib/samagotchi/web/private_files.rb +0 -31
|
@@ -8,7 +8,7 @@ module Samagotchi
|
|
|
8
8
|
# /objectives: with no arguments the session's list (Objectives), each
|
|
9
9
|
# objective's latest move under it (the model's objectives tool; a move
|
|
10
10
|
# changes nothing); `set`, `add`, `edit`, `done`, `drop` and `undo` edit it. An edit is a
|
|
11
|
-
# whole new list in a note file (NoteDelivery, with its refusal of a chi
|
|
11
|
+
# whole new list in a note file (NoteDelivery, with its refusal of a one-turn chi run's session), so it is
|
|
12
12
|
# safe from any thread, a turn running or not: it goes into the conversation
|
|
13
13
|
# at the worker's next pass between turns, and a second edit stacks on an
|
|
14
14
|
# unabsorbed first. Nothing here saves the session.
|
|
@@ -18,10 +18,6 @@ module Samagotchi
|
|
|
18
18
|
"(a text may end \"(done when: …)\"; lines work as | does)".freeze
|
|
19
19
|
NONE = "no objectives; #{NAME} set A | B sets them".freeze
|
|
20
20
|
BETWEEN_TURNS = "(an edit goes into the conversation between turns)"
|
|
21
|
-
SEPARATOR = /\n|(?:\A|\s)\|(?:\s|\z)/
|
|
22
|
-
# What the frame shows a status as: typed at a text's start it would pass
|
|
23
|
-
# for one (Objectives::Item#line).
|
|
24
|
-
STATUS_MARK = /\A(?:\[(?:#{Objectives::STATUSES.join("|")})\]\s*)+/i
|
|
25
21
|
# A proposal card's action (Tools::ObjectivesTool): the edit, then the card
|
|
26
22
|
# folds to a line, once: it is recorded, so a repeat applies nothing. A
|
|
27
23
|
# reword's --was says what it was proposed against. The prefix is the
|
|
@@ -30,7 +26,6 @@ module Samagotchi
|
|
|
30
26
|
FOLDS = { Objectives::APPLIED => "Proposal applied", Objectives::DISMISSED => "Proposal dismissed" }.freeze
|
|
31
27
|
DISMISS = "dismiss"
|
|
32
28
|
CHECK = /\Acheck(?:\s|\z)/i
|
|
33
|
-
MARKS = { "done" => Objectives::DONE, "drop" => Objectives::DROPPED, "undo" => Objectives::ACTIVE }.freeze
|
|
34
29
|
|
|
35
30
|
# @param engine [Engine] #session, #session_state_dir
|
|
36
31
|
def initialize(engine:)
|
|
@@ -84,7 +79,7 @@ module Samagotchi
|
|
|
84
79
|
end
|
|
85
80
|
|
|
86
81
|
def run_line(args, was: nil)
|
|
87
|
-
session = @engine.session or return [
|
|
82
|
+
session = @engine.session or return [Session::NONE_YET, :error]
|
|
88
83
|
# The running turn's moves aren't in session.messages yet. A snapshot from
|
|
89
84
|
# this thread, read only: an entry is there whole or not yet, and a call
|
|
90
85
|
# with no result yet pairs with nothing.
|
|
@@ -93,7 +88,7 @@ module Samagotchi
|
|
|
93
88
|
verb, rest = args.to_s.strip.split(/\s+/, 2)
|
|
94
89
|
return [listing(list, session), :ok] if verb.nil?
|
|
95
90
|
|
|
96
|
-
edited =
|
|
91
|
+
edited = Objectives::Edit.apply(list, verb.downcase, rest.to_s, was: was)
|
|
97
92
|
return [USAGE, :error] if edited.nil?
|
|
98
93
|
return [edited, :error] if edited.is_a?(String)
|
|
99
94
|
return ["#{listing(list, session)}\n(no change)", :ok] if edited == list
|
|
@@ -101,70 +96,10 @@ module Samagotchi
|
|
|
101
96
|
deliver(edited, session)
|
|
102
97
|
end
|
|
103
98
|
|
|
104
|
-
# @return [Objectives::List, String, nil] the new list, what's wrong with
|
|
105
|
-
# the line, or nil for a line that isn't one
|
|
106
|
-
def edit(list, verb, rest, was: nil)
|
|
107
|
-
case verb
|
|
108
|
-
when "set", "add"
|
|
109
|
-
entries = entries_of(rest)
|
|
110
|
-
return nil if entries.nil? || entries.empty?
|
|
111
|
-
|
|
112
|
-
verb == "set" ? list.set(entries) : entries.reduce(list) { |so_far, e| so_far.add(e[:text], done_when: e[:done_when]) }
|
|
113
|
-
when "edit" then reword(list, rest, was: was)
|
|
114
|
-
when *MARKS.keys then mark(list, MARKS.fetch(verb), rest)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def mark(list, status, rest)
|
|
119
|
-
ids = rest.upcase.split(/[\s,]+/).reject(&:empty?)
|
|
120
|
-
return nil if ids.empty?
|
|
121
|
-
|
|
122
|
-
missing = ids.reject { |id| list.find(id) }
|
|
123
|
-
return "no objective #{missing.join(", ")}" unless missing.empty?
|
|
124
|
-
|
|
125
|
-
if status == Objectives::DONE
|
|
126
|
-
dropped = ids.select { |id| list.find(id).status == Objectives::DROPPED }
|
|
127
|
-
return "#{dropped.join(", ")} is dropped; #{NAME} undo #{dropped.join(" ")} brings it back" unless dropped.empty?
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
ids.reduce(list) { |so_far, id| so_far.mark(id, status) }
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# The objective's new text: the whole rest of the line, a | too.
|
|
134
|
-
def reword(list, rest, was: nil)
|
|
135
|
-
id, text = rest.strip.split(/\s+/, 2)
|
|
136
|
-
entry = text && entry_of(text.gsub(/[[:space:]]+/, " ").strip)
|
|
137
|
-
return nil unless entry
|
|
138
|
-
|
|
139
|
-
id = id.upcase
|
|
140
|
-
item = list.find(id) or return "no objective #{id}"
|
|
141
|
-
return "#{id} has changed since this was proposed: #{item.text}" if was && Objectives.fingerprint(item) != was
|
|
142
|
-
|
|
143
|
-
list.reword(id, entry[:text], done_when: entry[:done_when])
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# One {text:, done_when:} per line or |-part, any whitespace collapsed
|
|
147
|
-
# (Unicode line separators too: the frame is one line per item).
|
|
148
|
-
# @return [Array<Hash>, nil] nil when a part has no text of its own
|
|
149
|
-
def entries_of(rest)
|
|
150
|
-
parts = rest.split(SEPARATOR).map { |part| part.gsub(/[[:space:]]+/, " ").strip }.reject(&:empty?)
|
|
151
|
-
entries = parts.map { |part| entry_of(part) }
|
|
152
|
-
entries.all? ? entries : nil
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def entry_of(part)
|
|
156
|
-
found = Objectives::DONE_WHEN.match(part)
|
|
157
|
-
text = (found ? found[1] : part).sub(STATUS_MARK, "").strip
|
|
158
|
-
return nil if text.empty?
|
|
159
|
-
|
|
160
|
-
done_when = found && found[2]
|
|
161
|
-
{ text: text, done_when: done_when.to_s.empty? ? nil : done_when }
|
|
162
|
-
end
|
|
163
|
-
|
|
164
99
|
def deliver(list, session)
|
|
165
100
|
result = NoteDelivery.deliver(session.id, text: list.text, source: Objectives::SOURCE,
|
|
166
101
|
state_dir: @engine.session_state_dir, objectives: list.to_payload)
|
|
167
|
-
return ["refused: #{
|
|
102
|
+
return ["refused: #{result.refusal}", :error] unless result.delivered?
|
|
168
103
|
|
|
169
104
|
@engine.announce_objectives(session)
|
|
170
105
|
["#{lines_of(list)}\n#{BETWEEN_TURNS}", :ok]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
module Objectives
|
|
5
|
+
# One /objectives edit line against a list, pure: what ObjectivesCommand
|
|
6
|
+
# applies and what a proposal (Proposal#refusal) is checked with, so the
|
|
7
|
+
# objectives tool refuses exactly what its card's Apply would. Reads the
|
|
8
|
+
# Objectives constants at a call, so objectives.rb can load it first.
|
|
9
|
+
module Edit
|
|
10
|
+
SEPARATOR = /\n|(?:\A|\s)\|(?:\s|\z)/
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# What the frame shows a status as: typed at a text's start it would pass
|
|
15
|
+
# for one (Item#line).
|
|
16
|
+
def status_mark = /\A(?:\[(?:#{Objectives::STATUSES.join("|")})\]\s*)+/i
|
|
17
|
+
|
|
18
|
+
# The verbs that set a status, and the status each sets.
|
|
19
|
+
def marks = { "done" => Objectives::DONE, "drop" => Objectives::DROPPED, "undo" => Objectives::ACTIVE }
|
|
20
|
+
|
|
21
|
+
# @param verb [String] set, add, edit, done, drop or undo, lower case
|
|
22
|
+
# @param rest [String] what follows it
|
|
23
|
+
# @param was [String, nil] what a reword was proposed against (Objectives.fingerprint)
|
|
24
|
+
# @return [List, String, nil] the new list, what's wrong with the line,
|
|
25
|
+
# or nil for a line that isn't one
|
|
26
|
+
def apply(list, verb, rest, was: nil)
|
|
27
|
+
case verb
|
|
28
|
+
when "set", "add"
|
|
29
|
+
entries = entries_of(rest)
|
|
30
|
+
return nil if entries.nil? || entries.empty?
|
|
31
|
+
|
|
32
|
+
verb == "set" ? list.set(entries) : entries.reduce(list) { |so_far, e| so_far.add(e[:text], done_when: e[:done_when]) }
|
|
33
|
+
when "edit" then reword(list, rest, was: was)
|
|
34
|
+
when *marks.keys then mark(list, marks.fetch(verb), rest)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def mark(list, status, rest)
|
|
39
|
+
ids = rest.upcase.split(/[\s,]+/).reject(&:empty?)
|
|
40
|
+
return nil if ids.empty?
|
|
41
|
+
|
|
42
|
+
missing = ids.reject { |id| list.find(id) }
|
|
43
|
+
return Objectives.unknown(missing, list) unless missing.empty?
|
|
44
|
+
|
|
45
|
+
if status == Objectives::DONE
|
|
46
|
+
dropped = ids.select { |id| list.find(id).status == Objectives::DROPPED }
|
|
47
|
+
return Objectives.dropped(dropped) unless dropped.empty?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
ids.reduce(list) { |so_far, id| so_far.mark(id, status) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The objective's new text: the whole rest of the line, a | too.
|
|
54
|
+
def reword(list, rest, was: nil)
|
|
55
|
+
id, text = rest.strip.split(/\s+/, 2)
|
|
56
|
+
entry = text && entry_of(text.gsub(/[[:space:]]+/, " ").strip)
|
|
57
|
+
return nil unless entry
|
|
58
|
+
|
|
59
|
+
id = id.upcase
|
|
60
|
+
item = list.find(id) or return Objectives.unknown(id, list)
|
|
61
|
+
return "#{id} has changed since this was proposed: #{item.text}" if was && Objectives.fingerprint(item) != was
|
|
62
|
+
|
|
63
|
+
list.reword(id, entry[:text], done_when: entry[:done_when])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# One {text:, done_when:} per line or |-part, any whitespace collapsed
|
|
67
|
+
# (Unicode line separators too: the frame is one line per item).
|
|
68
|
+
# @return [Array<Hash>, nil] nil when a part has no text of its own
|
|
69
|
+
def entries_of(rest)
|
|
70
|
+
parts = rest.split(SEPARATOR).map { |part| part.gsub(/[[:space:]]+/, " ").strip }.reject(&:empty?)
|
|
71
|
+
entries = parts.map { |part| entry_of(part) }
|
|
72
|
+
entries.all? ? entries : nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A part's text and done-when, nil when it has no text of its own (only a
|
|
76
|
+
# done-when, or only status marks). A leading status mark is stripped.
|
|
77
|
+
def entry_of(part)
|
|
78
|
+
found = Objectives::DONE_WHEN.match(part)
|
|
79
|
+
text = (found ? found[1] : part).sub(status_mark, "").strip
|
|
80
|
+
return nil if text.empty?
|
|
81
|
+
|
|
82
|
+
done_when = found && found[2]
|
|
83
|
+
{ text: text, done_when: done_when.to_s.empty? ? nil : done_when }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -62,6 +62,11 @@ module Samagotchi
|
|
|
62
62
|
|
|
63
63
|
# Qwen3.6 thinking blocks: <think>...</think> (including empty)
|
|
64
64
|
QWEN_THINK_RE = %r{<think>.*?</think>}m
|
|
65
|
+
# A Qwen thought cut off before it closed (no </think>), when the text
|
|
66
|
+
# opens with it (after any closed ones): the rest is thought, as with the
|
|
67
|
+
# Gemma channel. Only #strip_markup drops it; #remove_tokens (a streamed
|
|
68
|
+
# thought being shown) keeps it. A <think> mentioned mid-answer stays.
|
|
69
|
+
UNCLOSED_THINK_RE = /\A\s*<think>.*\z/m
|
|
65
70
|
# Qwen tool_call XML blocks that survive when a tool call is rendered as text
|
|
66
71
|
# e.g. <tool_call>\n<function=list_reminders>\n</function>\n</tool_call>
|
|
67
72
|
QWEN_TOOL_CALL_RE = %r{<tool_call>.*?</tool_call>}m
|
|
@@ -93,7 +98,8 @@ module Samagotchi
|
|
|
93
98
|
# a native Gemma step's closed tool-call blocks go with their bodies, as
|
|
94
99
|
# Qwen's do (a step that only calls a tool has no text).
|
|
95
100
|
def strip_markup(text)
|
|
96
|
-
|
|
101
|
+
text = text.to_s.gsub(GEMMA_TOOL_CALL_RE, "").gsub(QWEN_THINK_RE, "").sub(UNCLOSED_THINK_RE, "")
|
|
102
|
+
remove_tokens(text).gsub(/\n[ \t]*\n(?:[ \t]*\n)+/, "\n\n").strip
|
|
97
103
|
end
|
|
98
104
|
|
|
99
105
|
def remove_tokens(text)
|
|
@@ -16,6 +16,12 @@ module Samagotchi
|
|
|
16
16
|
# (even in the owner's own process) never releases the owner's lock.
|
|
17
17
|
class OwnerLock
|
|
18
18
|
FILE = "owner.lock"
|
|
19
|
+
# Who holds a "tui" lock now: a -p --non-interactive one-shot
|
|
20
|
+
# (TerminalUI), or an older chi's interactive REPL while an update is
|
|
21
|
+
# under way (N-1). In every refusal's words (SessionManager::OwnedByTUI
|
|
22
|
+
# and the callers that add what they refused); no parentheses: the
|
|
23
|
+
# web's continueRefusal reads "<short> (why)" out of a refusal.
|
|
24
|
+
TUI_BUSY = "in use by a one-turn chi --non-interactive run or an older chi's terminal"
|
|
19
25
|
DEFAULT_WAIT = 2.0
|
|
20
26
|
RETRY_INTERVAL = 0.05
|
|
21
27
|
OWNER_READ_ATTEMPTS = 10
|
|
@@ -101,7 +107,7 @@ module Samagotchi
|
|
|
101
107
|
# Wait for a leaving worker (Owner#leaving?) to release the lock: up to
|
|
102
108
|
# its leave_wait still to run plus LEAVE_MARGIN, looking every
|
|
103
109
|
# RELEASE_POLL seconds. An owner that isn't leaving (a live worker, a
|
|
104
|
-
# stuck one, a chi
|
|
110
|
+
# stuck one, a one-turn chi run) or no owner returns at once. A Ctrl-C (Interrupt)
|
|
105
111
|
# stops the wait as it would any sleep.
|
|
106
112
|
# @param on_wait [#call, nil] called once with the Owner before waiting
|
|
107
113
|
# @return [Symbol] :not_leaving, :released, or :timeout (still held)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "guardrails/parent_policy"
|
|
4
|
+
require_relative "session"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
6
7
|
# /parent-answers, the session's own guardrails.parent_answers
|
|
@@ -9,13 +10,13 @@ module Samagotchi
|
|
|
9
10
|
# soft) or `default` (none of its own) sets it, saved at once, for the next
|
|
10
11
|
# answer a parent gives. It may only tighten config.yml's
|
|
11
12
|
# (Guardrails::ParentPolicy.effective). Tightening goes from anyone;
|
|
12
|
-
# loosening (toward soft, or default) only from a user at a UI:
|
|
13
|
-
#
|
|
13
|
+
# loosening (toward soft, or default) only from a user at a UI: an
|
|
14
|
+
# attached TUI, the web (+user_ui+). A parent agent's line (chi send,
|
|
14
15
|
# a piped or marked attach, a delegate's follow-up) is refused.
|
|
15
16
|
class ParentAnswersCommand
|
|
16
17
|
NAME = "/parent-answers"
|
|
17
18
|
USAGE = "usage: #{NAME} [hard|standard|soft|default]".freeze
|
|
18
|
-
REFUSED = "loosening parent_answers is the user's: #{NAME} in chi
|
|
19
|
+
REFUSED = "loosening parent_answers is the user's: #{NAME} in a chi terminal or the web " \
|
|
19
20
|
"(not chi send or a parent agent)".freeze
|
|
20
21
|
|
|
21
22
|
# What +args+ (the text after the name) asks for.
|
|
@@ -51,7 +52,7 @@ module Samagotchi
|
|
|
51
52
|
# @return [Array(String, Boolean, Symbol)] the reply, whether the
|
|
52
53
|
# session's value changed, and :ok or :error
|
|
53
54
|
def run(args, user_ui:)
|
|
54
|
-
session = @engine.session or return [
|
|
55
|
+
session = @engine.session or return [Session::NONE_YET, false, :error]
|
|
55
56
|
kind, value = self.class.parse(args)
|
|
56
57
|
return [USAGE, false, :error] if kind == :usage
|
|
57
58
|
return [listing(session), false, :ok] if kind == :show
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# What a participant may learn of their review's usage, by their grant's
|
|
5
|
+
# exposure level (Grants::Grant#exposure; the review-composer plan's D3):
|
|
6
|
+
# the policy that maps a level to the usage fields the review route
|
|
7
|
+
# gives them (Web::ParticipantReview#review_usage). One level today:
|
|
8
|
+
#
|
|
9
|
+
# - +own+ (every grant's): their own spend and cap, their own queued
|
|
10
|
+
# turns and queue limit, their link's expiry, the session's model.
|
|
11
|
+
# Never another participant's or the owner's usage, nor the session's.
|
|
12
|
+
#
|
|
13
|
+
# A later level (a tier that also shows the session's spend, say) adds
|
|
14
|
+
# its row to LEVELS and its fields' sources in Web::ParticipantReview: one
|
|
15
|
+
# place each. A level this chi doesn't know (written by a newer chi, or
|
|
16
|
+
# a hand-edited store) reads as DEFAULT, the narrowest: an unknown level
|
|
17
|
+
# never shows more. See docs/internals/participants.md (Exposure).
|
|
18
|
+
module ParticipantExposure
|
|
19
|
+
OWN = "own"
|
|
20
|
+
DEFAULT = OWN
|
|
21
|
+
# level => the usage fields it shows, in the order the page reads them.
|
|
22
|
+
LEVELS = {
|
|
23
|
+
OWN => %i[spent cap queued queue_limit expires_at model].freeze
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
# +value+ as a level this chi knows, else DEFAULT.
|
|
27
|
+
def self.level(value) = LEVELS.key?(value) ? value : DEFAULT
|
|
28
|
+
|
|
29
|
+
# @return [Array<Symbol>] the fields +value+'s level shows
|
|
30
|
+
def self.fields(value) = LEVELS.fetch(level(value))
|
|
31
|
+
|
|
32
|
+
# The usage +value+'s level shows, each field read from +sources+
|
|
33
|
+
# (field => a callable): only the fields shown are computed, and a
|
|
34
|
+
# field a level names with no source raises (KeyError), never shows
|
|
35
|
+
# nothing silently.
|
|
36
|
+
# @param sources [Hash{Symbol => #call}]
|
|
37
|
+
# @return [Hash{Symbol => Object}]
|
|
38
|
+
def self.usage(value, sources)
|
|
39
|
+
fields(value).to_h { |field| [field, sources.fetch(field).call] }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -38,7 +38,7 @@ module Samagotchi
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# The mode a turn of the participant +entry+ (a Participants::Entry or a
|
|
41
|
-
#
|
|
41
|
+
# Grants::Grant; nil: unknown) runs under in +session+.
|
|
42
42
|
# @return [Symbol]
|
|
43
43
|
def for_turn(session, entry, features: nil)
|
|
44
44
|
return REVIEW unless of(session, features: features) == PAIR
|
|
@@ -5,7 +5,7 @@ module Samagotchi
|
|
|
5
5
|
# line of at most MAX characters with no brackets (nor Unicode line
|
|
6
6
|
# breaks, nor full-width square brackets), so a name can't close or forge
|
|
7
7
|
# the review-contribution header (Steer.review_header). The grants store
|
|
8
|
-
# cleans a name at issue (
|
|
8
|
+
# cleans a name at issue (Grants.clean_name); the header cleans it
|
|
9
9
|
# again, whatever a session file or a sidecar holds.
|
|
10
10
|
module ParticipantName
|
|
11
11
|
MAX = 64
|
|
@@ -7,12 +7,12 @@ require_relative "atomic_file"
|
|
|
7
7
|
require_relative "participant_messages"
|
|
8
8
|
require_relative "archive_store"
|
|
9
9
|
require_relative "session"
|
|
10
|
-
require_relative "
|
|
10
|
+
require_relative "grants"
|
|
11
11
|
|
|
12
12
|
module Samagotchi
|
|
13
13
|
# A session's participants: the people (or agents) its owner invited to
|
|
14
14
|
# review it (`chi share`, the owner's participants routes), each through a
|
|
15
|
-
#
|
|
15
|
+
# Grants grant. Two files in the session dir, neither in the session
|
|
16
16
|
# JSON (chi web never saves that, and a worker would overwrite a field it
|
|
17
17
|
# added):
|
|
18
18
|
# - participants.json, the sidecar: who is or was invited, never a token
|
|
@@ -62,7 +62,7 @@ module Samagotchi
|
|
|
62
62
|
[]
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
# The sidecar rewritten from +grants+ (that session's
|
|
65
|
+
# The sidecar rewritten from +grants+ (that session's Grants::Grant
|
|
66
66
|
# list). Nothing is written for a session without grants, nor for one
|
|
67
67
|
# whose dir is gone.
|
|
68
68
|
def write(session_id, grants, state_dir: Session.default_state_dir)
|
|
@@ -79,7 +79,7 @@ module Samagotchi
|
|
|
79
79
|
AtomicFile.write(File.join(dir, FILE), "#{JSON.pretty_generate({ "participants" => entries })}\n", perm: 0o600)
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
#
|
|
82
|
+
# Grants' on_write for a change to +session_ids+ (nil: the sessions
|
|
83
83
|
# of the grants it changed, a revoke's result): their sidecars rewritten
|
|
84
84
|
# from the grants just written, under the store's lock (two writers, chi
|
|
85
85
|
# web and chi share, can't finish out of order and lose a revoke here).
|
|
@@ -100,21 +100,21 @@ module Samagotchi
|
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
# Invites a participant to +session_id+: the grant (+grants+, a
|
|
103
|
-
#
|
|
103
|
+
# Grants), the sidecar and the audit line. A grant bound to
|
|
104
104
|
# +identity+ needs +public_url+ (the chi web's Web::PublicUrl) with an
|
|
105
105
|
# identity header: only a request through that proxy can carry it.
|
|
106
|
-
# @return [
|
|
107
|
-
# @raise [
|
|
106
|
+
# @return [Grants::Issued]
|
|
107
|
+
# @raise [Grants::Invalid] an archived session, or bad values
|
|
108
108
|
# (Unbindable: an identity without such a public_url)
|
|
109
109
|
# @raise [ArgumentError] no such session
|
|
110
110
|
def invite(grants, session_id:, name:, kind: "human", ttl: nil, cap: nil, identity: nil, public_url: nil,
|
|
111
111
|
mode: nil, state_dir: Session.default_state_dir)
|
|
112
112
|
raise ArgumentError, "Session not found: #{session_id}" unless File.exist?(Session.session_file(session_id, state_dir: state_dir))
|
|
113
113
|
|
|
114
|
-
identity =
|
|
115
|
-
raise
|
|
114
|
+
identity = Grants.clean_identity(identity) unless identity.nil?
|
|
115
|
+
raise Grants::Unbindable if !identity.nil? && public_url&.identity_header.nil?
|
|
116
116
|
if ArchiveStore.archived?(Session.session_dir(session_id, state_dir: state_dir))
|
|
117
|
-
raise
|
|
117
|
+
raise Grants::Invalid, "session #{session_id[0, 8]} is archived: unarchive it to invite anyone"
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
issued = grants.issue(session_id: session_id, name: name, kind: kind, ttl: ttl, cap: cap, identity: identity,
|
|
@@ -128,8 +128,8 @@ module Samagotchi
|
|
|
128
128
|
|
|
129
129
|
# Sets participant +id+'s cap on +session_id+ (chi share cap): the
|
|
130
130
|
# grant, the sidecar (the worker's check) and the audit line.
|
|
131
|
-
# @return [
|
|
132
|
-
# @raise [
|
|
131
|
+
# @return [Grants::Grant, nil] nil when +id+ isn't an active grant there
|
|
132
|
+
# @raise [Grants::Invalid] a cap out of range
|
|
133
133
|
def set_cap(grants, session_id:, id:, cap:, state_dir: Session.default_state_dir)
|
|
134
134
|
grant = grants.set_cap(id, cap, session_id: session_id, on_write: sidecar_writer(state_dir, [session_id]))
|
|
135
135
|
return nil unless grant
|
|
@@ -142,9 +142,9 @@ module Samagotchi
|
|
|
142
142
|
# +session_id+.
|
|
143
143
|
def no_participant(id, session_id) = "no active participant #{id} in #{session_id.to_s[0, 8]} (chi share ls lists them)"
|
|
144
144
|
|
|
145
|
-
# Revokes grants (
|
|
145
|
+
# Revokes grants (Grants#revoke's arguments) and brings each
|
|
146
146
|
# affected session's sidecar and audit trail along.
|
|
147
|
-
# @return [Array<
|
|
147
|
+
# @return [Array<Grants::Grant>] the grants it revoked
|
|
148
148
|
def revoke(grants, session_id: nil, id: nil, by: "owner", state_dir: Session.default_state_dir)
|
|
149
149
|
revoked = grants.revoke(session_id: session_id, id: id, by: by, on_write: sidecar_writer(state_dir))
|
|
150
150
|
revoked.group_by(&:session_id).each do |sid, ended|
|
|
@@ -156,15 +156,15 @@ module Samagotchi
|
|
|
156
156
|
# Ends the grants of sessions that were archived or deleted
|
|
157
157
|
# (SessionManager): +by+ "archive" or "delete". Nothing is written for a
|
|
158
158
|
# session nobody was invited to.
|
|
159
|
-
# A grants store that doesn't read (
|
|
159
|
+
# A grants store that doesn't read (Grants::Corrupt) doesn't stop
|
|
160
160
|
# the archive or delete: its links already find no grant, and the
|
|
161
161
|
# store itself says so (Log.warn) until it's fixed. Nor does one a newer
|
|
162
162
|
# chi wrote (Newer): chi web's gate refuses a grant whose session is
|
|
163
163
|
# archived or gone even when it can't write the revoke.
|
|
164
164
|
def end_sessions(session_ids, by:, state_dir: Session.default_state_dir)
|
|
165
|
-
grants =
|
|
165
|
+
grants = Grants.new(Grants.path(state_dir: state_dir))
|
|
166
166
|
session_ids.each { |sid| revoke(grants, session_id: sid, by: by, state_dir: state_dir) }
|
|
167
|
-
rescue
|
|
167
|
+
rescue Grants::Unwritable
|
|
168
168
|
nil
|
|
169
169
|
end
|
|
170
170
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Samagotchi
|
|
4
|
-
# Loaded only when a sidecar is read: it brings
|
|
4
|
+
# Loaded only when a sidecar is read: it brings Grants.
|
|
5
5
|
autoload :Participants, File.expand_path("participants", __dir__)
|
|
6
6
|
|
|
7
7
|
# Who is in a session's review now, and who came or went since the last
|
|
@@ -19,8 +19,8 @@ module Samagotchi
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# Append a message to the queue. Nil/empty strings are ignored.
|
|
22
|
-
# +seq+ is the sender's own order number for it (
|
|
23
|
-
# line it takes during a turn, so its leftovers keep send order across
|
|
22
|
+
# +seq+ is the sender's own order number for it (a sender that numbers
|
|
23
|
+
# every line it takes during a turn, so its leftovers keep send order across
|
|
24
24
|
# this queue and its after-turn list: #drain_entries).
|
|
25
25
|
def push(text, seq: nil)
|
|
26
26
|
text = text.to_s
|
|
@@ -56,7 +56,8 @@ module Samagotchi
|
|
|
56
56
|
"ctx.context.propose" => "0.140.0", "ctx.context.candidates" => "0.140.0", "ctx.context.withdraw" => "0.140.0",
|
|
57
57
|
"ctx.continues" => "0.141.0", "ctx.context.left_behind" => "0.141.0", "ctx.context.attach(movable:)" => "0.141.0",
|
|
58
58
|
"ctx.session_dir" => "0.143.0", "ctx.session_dir(create:)" => "0.144.0",
|
|
59
|
-
"ctx.objectives" => "0.149.0", "ctx.turn_running?" => "0.149.0"
|
|
59
|
+
"ctx.objectives" => "0.149.0", "ctx.turn_running?" => "0.149.0",
|
|
60
|
+
"ctx.last_answer" => "0.154.0"
|
|
60
61
|
}.freeze
|
|
61
62
|
|
|
62
63
|
# @param bundle [String]
|
|
@@ -234,7 +235,7 @@ module Samagotchi
|
|
|
234
235
|
# says how to stop it. It starts on first svc.value, or now with
|
|
235
236
|
# eager: true (a raise then fails the plugin's load, unless the
|
|
236
237
|
# plugin rescues it). The Engine stops its services when it shuts
|
|
237
|
-
# down (the
|
|
238
|
+
# down (the session's worker or a one-shot exits), newest first.
|
|
238
239
|
# @param name [String, Symbol] unique in the plugin
|
|
239
240
|
# @return [Service]
|
|
240
241
|
def service(name, eager: false, &block)
|
|
@@ -18,6 +18,9 @@ module Samagotchi
|
|
|
18
18
|
autoload :PluginSessionState, File.expand_path("../plugin_session_state", __dir__)
|
|
19
19
|
# Context#objectives reads it; the kit shouldn't carry its files.
|
|
20
20
|
autoload :Objectives, File.expand_path("../objectives", __dir__)
|
|
21
|
+
# Context#last_answer reads them.
|
|
22
|
+
autoload :AnswerTail, File.expand_path("../answer_tail", __dir__)
|
|
23
|
+
autoload :AnswerText, File.expand_path("../answer_text", __dir__)
|
|
21
24
|
|
|
22
25
|
module Plugin
|
|
23
26
|
# Loaded on first ctx.sessions / ctx.context (or a bundle naming
|
|
@@ -50,7 +53,7 @@ module Samagotchi
|
|
|
50
53
|
# model's memory overlay key (ctx.model, ctx.model_key).
|
|
51
54
|
# +steer+ takes (text, label), +stop_turn+ and +stop_generation+
|
|
52
55
|
# (reason, label), each true when it acted on a running turn.
|
|
53
|
-
# +frontend+ returns the Engine's interface (:
|
|
56
|
+
# +frontend+ returns the Engine's interface (:worker or
|
|
54
57
|
# :non_interactive), which ctx.frontend names for plugins.
|
|
55
58
|
# +toolkit+ returns the session's toolkit name, +declared_tools+ the
|
|
56
59
|
# tool names its next turn declares, +use_toolkit+ takes a name and
|
|
@@ -260,12 +263,12 @@ module Samagotchi
|
|
|
260
263
|
def scratch? = !!@host.scratch&.call
|
|
261
264
|
|
|
262
265
|
# ctx.frontend's values, by the Engine's interface.
|
|
263
|
-
FRONTENDS = {
|
|
266
|
+
FRONTENDS = { non_interactive: :one_shot, worker: :worker }.freeze
|
|
264
267
|
|
|
265
268
|
# What runs the session, read now (the host sets it after the plugins
|
|
266
|
-
# load): :
|
|
267
|
-
# (
|
|
268
|
-
#
|
|
269
|
+
# load): :one_shot (-p --non-interactive: one turn, then exit) or
|
|
270
|
+
# :worker (a session worker: the web, an attached TUI). A host without
|
|
271
|
+
# one is :worker.
|
|
269
272
|
# @return [Symbol]
|
|
270
273
|
def frontend = FRONTENDS.fetch(@host.frontend&.call, :worker)
|
|
271
274
|
|
|
@@ -316,19 +319,30 @@ module Samagotchi
|
|
|
316
319
|
# The conversation so far, a frozen copy, without the system prompt.
|
|
317
320
|
# While a turn runs, a session worker's (attached TUI, web) adds the
|
|
318
321
|
# turn so far: its prompt, the model's text and the lines merged into
|
|
319
|
-
# it;
|
|
322
|
+
# it; a one-shot's is the conversation before that turn
|
|
320
323
|
# (#messages_partial?).
|
|
321
324
|
# @return [Array<Hash>]
|
|
322
325
|
def messages
|
|
323
326
|
Array(@host.messages.call).map { |message| message.dup.freeze }.freeze
|
|
324
327
|
end
|
|
325
328
|
|
|
329
|
+
# The newest answer's text as the user saw it (AnswerText.visible): the
|
|
330
|
+
# newest bubble of #messages, its thoughts (a cut-off one too) and call
|
|
331
|
+
# markup out. Mid-turn a worker's can be the running turn's interim text.
|
|
332
|
+
# @return [String, nil] nil when there is none, or it was a thought alone
|
|
333
|
+
def last_answer
|
|
334
|
+
found = AnswerTail.find(messages)
|
|
335
|
+
content = found && (found[:content] || found["content"])
|
|
336
|
+
text = content.is_a?(String) ? AnswerText.visible(content) : ""
|
|
337
|
+
text.empty? ? nil : text
|
|
338
|
+
end
|
|
339
|
+
|
|
326
340
|
# Whether a turn runs now. Its :after_turn hooks run inside it, so a thread
|
|
327
341
|
# one of them starts can use this to wait for the end: a card shown before
|
|
328
342
|
# then belongs to that turn's block.
|
|
329
343
|
def turn_running? = !!@host.turn_running&.call
|
|
330
344
|
|
|
331
|
-
# Whether #messages leaves out a running turn (
|
|
345
|
+
# Whether #messages leaves out a running turn (a one-shot mid-turn), so
|
|
332
346
|
# a plugin can say what its answer is about.
|
|
333
347
|
def messages_partial? = !!@host.messages_partial&.call
|
|
334
348
|
|
|
@@ -6,6 +6,7 @@ require "open3"
|
|
|
6
6
|
require "tempfile"
|
|
7
7
|
require "time"
|
|
8
8
|
require_relative "../paths"
|
|
9
|
+
require_relative "../session"
|
|
9
10
|
|
|
10
11
|
module Samagotchi
|
|
11
12
|
module Plugin
|
|
@@ -149,7 +150,7 @@ module Samagotchi
|
|
|
149
150
|
def pair_again(ctx)
|
|
150
151
|
peer = self.class
|
|
151
152
|
previous = ctx.continues or return "not a continue"
|
|
152
|
-
id = ctx.session_id or return
|
|
153
|
+
id = ctx.session_id or return Session::NONE_YET
|
|
153
154
|
left = ctx.context.left_behind.find { |row| row.name == peer::INBOX }
|
|
154
155
|
hint = left&.hint.to_s.strip
|
|
155
156
|
return "no #{peer::INBOX} left behind" if hint.empty?
|
|
@@ -110,7 +110,7 @@ module Samagotchi
|
|
|
110
110
|
# @param delivery [Symbol, String, nil] nil or :next_step (default),
|
|
111
111
|
# :queue
|
|
112
112
|
# @return [String] the id of the session it went to
|
|
113
|
-
# @raise [Error] no such session, a
|
|
113
|
+
# @raise [Error] no such session, a one-shot owns it, it wasn't taken
|
|
114
114
|
# (a worker older than queueing refuses a queue), or delivery is
|
|
115
115
|
# :cut or unknown
|
|
116
116
|
def send(id, text, delivery: nil)
|
|
@@ -128,8 +128,10 @@ module Samagotchi
|
|
|
128
128
|
when :timeout then raise Error, "session #{sid[0, 8]}'s worker did not answer in time; the message was not sent"
|
|
129
129
|
else raise Error, "the message to session #{sid[0, 8]} could not be written"
|
|
130
130
|
end
|
|
131
|
+
rescue SessionManager::ScratchSession => e
|
|
132
|
+
raise Error, "session #{sid[0, 8]}: #{e.message}"
|
|
131
133
|
rescue SessionManager::OwnedByTUI
|
|
132
|
-
raise Error, "session #{sid[0, 8]} is
|
|
134
|
+
raise Error, "session #{sid[0, 8]} is #{OwnerLock::TUI_BUSY}, which takes no messages from others"
|
|
133
135
|
end
|
|
134
136
|
|
|
135
137
|
# The wire value for send's delivery: (nil for the default).
|
|
@@ -182,7 +184,7 @@ module Samagotchi
|
|
|
182
184
|
# its worker to let go.
|
|
183
185
|
# @return [String] the child's id
|
|
184
186
|
# @raise [Error] not a child of this session, no such session, or a
|
|
185
|
-
#
|
|
187
|
+
# one-shot owns it
|
|
186
188
|
def stop(id)
|
|
187
189
|
state_dir = self.state_dir
|
|
188
190
|
sid = resolve(id, state_dir)
|
|
@@ -194,7 +196,7 @@ module Samagotchi
|
|
|
194
196
|
SessionManager.stop_session(sid, state_dir: state_dir, wait: STOP_WAIT)
|
|
195
197
|
sid
|
|
196
198
|
rescue SessionManager::OwnedByTUI
|
|
197
|
-
raise Error, "session #{sid[0, 8]} is
|
|
199
|
+
raise Error, "session #{sid[0, 8]} is #{OwnerLock::TUI_BUSY}; let it finish"
|
|
198
200
|
end
|
|
199
201
|
|
|
200
202
|
private
|
|
@@ -70,8 +70,7 @@ module Samagotchi
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# Start the init tasks not started yet, each on its own thread. The
|
|
73
|
-
# worker calls it once its Bridge is up,
|
|
74
|
-
# events, and every turn (a -p run has only that); later calls start
|
|
73
|
+
# worker calls it once its Bridge is up, and every turn (a -p run has only that); later calls start
|
|
75
74
|
# nothing new.
|
|
76
75
|
def start!
|
|
77
76
|
@mutex.synchronize do
|