samagotchi 0.2.0 → 0.4.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 +198 -1
- data/README.md +56 -4
- data/bin/chi +118 -50
- data/docs/cli.md +184 -9
- data/docs/configuration.md +333 -47
- data/docs/desktop.md +45 -4
- data/docs/guardrails.md +11 -0
- data/docs/hooks.md +208 -5
- data/docs/plugins.md +68 -2
- data/docs/releasing.md +23 -13
- data/docs/sessions.md +45 -17
- data/lib/samagotchi/answer_display.rb +95 -0
- data/lib/samagotchi/archive_store.rb +90 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +342 -0
- data/lib/samagotchi/bootstrap/probe.rb +262 -0
- data/lib/samagotchi/bootstrap_command.rb +347 -0
- data/lib/samagotchi/bridge/pending_card.rb +89 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +15 -3
- data/lib/samagotchi/bridge.rb +13 -1
- data/lib/samagotchi/bridge_client.rb +6 -2
- data/lib/samagotchi/bundles/check-in/manifest.yml +10 -0
- data/lib/samagotchi/bundles/check-in/plugin.rb +244 -0
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +531 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +14 -0
- data/lib/samagotchi/bundles/source-links/source_links.md +5 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +10 -6
- data/lib/samagotchi/bundles/system/delegated.md +6 -7
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/self_map.md +8 -2
- data/lib/samagotchi/client.rb +81 -19
- data/lib/samagotchi/commands/registry.rb +8 -0
- data/lib/samagotchi/config.rb +252 -48
- data/lib/samagotchi/desktop/macos/App.swift +12 -8
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +17 -9
- data/lib/samagotchi/desktop/macos/Images.swift +113 -0
- data/lib/samagotchi/desktop/macos/Info.plist.erb +6 -0
- data/lib/samagotchi/desktop/macos/Panel.swift +180 -25
- data/lib/samagotchi/desktop/macos.rb +59 -8
- data/lib/samagotchi/desktop_command.rb +6 -3
- data/lib/samagotchi/edit_preview.rb +82 -0
- data/lib/samagotchi/empty_answer_retry.rb +43 -0
- data/lib/samagotchi/engine.rb +434 -140
- data/lib/samagotchi/gem_update.rb +89 -0
- data/lib/samagotchi/guardrails/approval.rb +35 -4
- data/lib/samagotchi/guardrails/load_failures.rb +9 -3
- data/lib/samagotchi/guardrails/scratch_writes.rb +40 -0
- data/lib/samagotchi/guardrails.rb +1 -0
- data/lib/samagotchi/hooks/registry.rb +24 -5
- data/lib/samagotchi/host_registry.rb +9 -12
- data/lib/samagotchi/idle_client.rb +24 -15
- data/lib/samagotchi/idle_recap.rb +5 -1
- data/lib/samagotchi/idle_reminders.rb +2 -2
- data/lib/samagotchi/image_store.rb +10 -6
- data/lib/samagotchi/kernel_loop.rb +73 -94
- data/lib/samagotchi/live_versions.rb +59 -0
- data/lib/samagotchi/llm/api_key.rb +41 -0
- data/lib/samagotchi/llm/chat_loop.rb +132 -29
- data/lib/samagotchi/llm/errors.rb +41 -9
- data/lib/samagotchi/llm/http.rb +57 -17
- data/lib/samagotchi/llm/openai_chat.rb +17 -30
- data/lib/samagotchi/log_subscriber.rb +18 -3
- data/lib/samagotchi/memory_bundle/installer.rb +65 -63
- data/lib/samagotchi/memory_bundle/provenance.rb +51 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +157 -0
- data/lib/samagotchi/memory_bundle/status.rb +4 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +81 -53
- data/lib/samagotchi/model_profile.rb +24 -1
- data/lib/samagotchi/plugin/context.rb +22 -1
- data/lib/samagotchi/plugin/sessions.rb +3 -1
- data/lib/samagotchi/prompt.rb +4 -2
- data/lib/samagotchi/reminder_store.rb +1 -9
- data/lib/samagotchi/reply_wait.rb +126 -0
- data/lib/samagotchi/sampling_settings.rb +58 -0
- data/lib/samagotchi/self_report.rb +18 -3
- data/lib/samagotchi/send_command.rb +252 -11
- data/lib/samagotchi/session.rb +52 -11
- data/lib/samagotchi/session_archive_command.rb +107 -0
- data/lib/samagotchi/session_commands.rb +46 -7
- data/lib/samagotchi/session_manager.rb +115 -25
- data/lib/samagotchi/session_metrics.rb +222 -106
- data/lib/samagotchi/steer.rb +72 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +57 -28
- data/lib/samagotchi/terminal_ui/event_renderer.rb +21 -11
- data/lib/samagotchi/terminal_ui/formatting.rb +40 -8
- data/lib/samagotchi/terminal_ui/input_support.rb +7 -19
- data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
- data/lib/samagotchi/terminal_ui.rb +134 -247
- data/lib/samagotchi/text_diff.rb +181 -0
- data/lib/samagotchi/thinking.rb +115 -0
- data/lib/samagotchi/tool_activity.rb +3 -1
- data/lib/samagotchi/tool_runner.rb +34 -1
- data/lib/samagotchi/tools/ask_user_question.rb +41 -33
- data/lib/samagotchi/tools/builtins.rb +15 -4
- data/lib/samagotchi/tools/delegate_wait.rb +26 -69
- data/lib/samagotchi/tools/edit.rb +23 -9
- data/lib/samagotchi/tools/execute.rb +52 -14
- data/lib/samagotchi/tools/task_runtime.rb +19 -0
- data/lib/samagotchi/tools/task_wait.rb +27 -3
- data/lib/samagotchi/tools/write.rb +4 -0
- data/lib/samagotchi/turn_flow.rb +12 -2
- data/lib/samagotchi/turn_note.rb +60 -6
- data/lib/samagotchi/update_command.rb +308 -0
- data/lib/samagotchi/update_hint.rb +59 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +7 -9
- data/lib/samagotchi/web/app.rb +91 -7
- data/lib/samagotchi/web/message_parts.rb +8 -3
- data/lib/samagotchi/web/public/activity.js +13 -1
- data/lib/samagotchi/web/public/annotate_presets.js +26 -0
- data/lib/samagotchi/web/public/annotations.js +13 -0
- data/lib/samagotchi/web/public/app.js +472 -111
- data/lib/samagotchi/web/public/card.js +5 -3
- data/lib/samagotchi/web/public/chat_view.js +13 -1
- data/lib/samagotchi/web/public/copy.js +20 -4
- data/lib/samagotchi/web/public/ctx.js +15 -0
- data/lib/samagotchi/web/public/data.js +23 -6
- data/lib/samagotchi/web/public/diff_view.js +58 -0
- data/lib/samagotchi/web/public/format.js +9 -0
- data/lib/samagotchi/web/public/index.html +60 -3
- data/lib/samagotchi/web/public/notify.js +175 -0
- data/lib/samagotchi/web/public/question_card.js +5 -2
- data/lib/samagotchi/web/public/sessions_list.js +7 -0
- data/lib/samagotchi/web/public/timing.js +39 -14
- data/lib/samagotchi/web/public/turn_events.js +75 -5
- data/lib/samagotchi/web/public/turn_view.js +49 -8
- data/lib/samagotchi/web/server.rb +8 -4
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_summary.rb +24 -1
- data/lib/samagotchi/worker.rb +16 -4
- metadata +31 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../kernel_loop"
|
|
4
|
+
require_relative "../steer"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
6
7
|
class Bridge
|
|
@@ -32,7 +33,7 @@ module Samagotchi
|
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
# @return [Hash, nil] a copy of the turn in progress: prompt, origin,
|
|
35
|
-
# continue, ordered parts (thinking / text / tool / input / reminder / hook_notice),
|
|
36
|
+
# continue, ordered parts (thinking / text / tool / input / steer / reminder / hook_notice),
|
|
36
37
|
# pending_question and the last event_seq folded in
|
|
37
38
|
def current_turn
|
|
38
39
|
@mutex.synchronize { @turn && Marshal.load(Marshal.dump(@turn)) }
|
|
@@ -67,6 +68,9 @@ module Samagotchi
|
|
|
67
68
|
when "input"
|
|
68
69
|
flush.call
|
|
69
70
|
messages << { role: "user", content: part[:text].to_s }
|
|
71
|
+
when "steer"
|
|
72
|
+
flush.call
|
|
73
|
+
messages << Steer.message(text: part[:text], source: part[:source])
|
|
70
74
|
end
|
|
71
75
|
end
|
|
72
76
|
flush.call
|
|
@@ -155,9 +159,17 @@ module Samagotchi
|
|
|
155
159
|
tool[:output] = capped ? output[0, @max_output_chars] : output.dup
|
|
156
160
|
tool[:output_truncated] = capped || !!event[:output_truncated]
|
|
157
161
|
tool[:images] = event[:images] if event[:images]
|
|
162
|
+
tool[:diff] = event[:diff] if event[:diff]
|
|
158
163
|
when :pending_input_merged
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
# A steer-only merge (count 0) has no user part: the origins stay
|
|
165
|
+
# for the user lines' own merge.
|
|
166
|
+
unless event[:content].nil?
|
|
167
|
+
parts << { kind: "input", iteration: event[:iteration], text: event[:content].to_s.dup, origins: @merged_origins }
|
|
168
|
+
@merged_origins = []
|
|
169
|
+
end
|
|
170
|
+
Array(event[:steers]).each do |steer|
|
|
171
|
+
parts << { kind: "steer", iteration: event[:iteration], source: steer[:source].to_s, text: steer[:text].to_s.dup }
|
|
172
|
+
end
|
|
161
173
|
when :reminder_injected
|
|
162
174
|
parts << { kind: "reminder", reminders: Array(event[:reminders]).map(&:dup) }
|
|
163
175
|
when :hook_notice
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -10,6 +10,7 @@ require "time"
|
|
|
10
10
|
require_relative "bridge/bounded_queue"
|
|
11
11
|
require_relative "bridge/card_store"
|
|
12
12
|
require_relative "bridge/event_id"
|
|
13
|
+
require_relative "bridge/pending_card"
|
|
13
14
|
require_relative "bridge/ring_buffer"
|
|
14
15
|
require_relative "bridge/sse_writer"
|
|
15
16
|
require_relative "bridge/turn_accumulator"
|
|
@@ -18,6 +19,7 @@ require_relative "engine"
|
|
|
18
19
|
require_relative "session_commands"
|
|
19
20
|
require_relative "image_store"
|
|
20
21
|
require_relative "log"
|
|
22
|
+
require_relative "version"
|
|
21
23
|
|
|
22
24
|
module Samagotchi
|
|
23
25
|
# Bridge is an optional HTTP transport that lets an external web / desktop
|
|
@@ -92,6 +94,7 @@ module Samagotchi
|
|
|
92
94
|
@ring = RingBuffer.new(capacity: ring_capacity)
|
|
93
95
|
@accumulator = TurnAccumulator.new
|
|
94
96
|
@cards = CardStore.new
|
|
97
|
+
@pending_card = PendingCard.new(session_dir)
|
|
95
98
|
@heartbeat_interval = heartbeat_interval
|
|
96
99
|
@epoch = SecureRandom.hex(4)
|
|
97
100
|
|
|
@@ -151,6 +154,9 @@ module Samagotchi
|
|
|
151
154
|
# A plugin's ctx.messages mid-turn holds the turn so far (plan O1).
|
|
152
155
|
@engine.running_turn_messages = -> { @accumulator.current_messages } if @engine.respond_to?(:running_turn_messages=)
|
|
153
156
|
@cards_handle = @engine.subscribe(observer: @cards)
|
|
157
|
+
# One a worker that died left is not open.
|
|
158
|
+
@pending_card.clear
|
|
159
|
+
@pending_card_handle = @engine.subscribe(observer: @pending_card)
|
|
154
160
|
@accept_thread = Thread.new { accept_loop }
|
|
155
161
|
@accept_thread.report_on_exception = false
|
|
156
162
|
write_sidecar
|
|
@@ -175,6 +181,8 @@ module Samagotchi
|
|
|
175
181
|
@accumulator_handle&.unsubscribe
|
|
176
182
|
@engine.running_turn_messages = nil if @engine.respond_to?(:running_turn_messages=)
|
|
177
183
|
@cards_handle&.unsubscribe
|
|
184
|
+
@pending_card_handle&.unsubscribe
|
|
185
|
+
@pending_card&.clear
|
|
178
186
|
# A turn post killed between its enqueue and its reply looks failed to
|
|
179
187
|
# the web, which then queues the prompt again from the input file.
|
|
180
188
|
await_answers(REQUEST_GRACE_SECONDS)
|
|
@@ -597,6 +605,8 @@ module Samagotchi
|
|
|
597
605
|
return BAD_DEADLINE unless deadline_valid?(deadline)
|
|
598
606
|
|
|
599
607
|
command = { command_id: SecureRandom.uuid, client_id: fetched(parsed, "client_id"), line: line }
|
|
608
|
+
# A card's action: the UIs leave its line out (the card is the echo).
|
|
609
|
+
command[:card] = true if fetched(parsed, "card") == true
|
|
600
610
|
queued = @engine.synchronize_events do
|
|
601
611
|
next false if expired?("command_expired", deadline, sid: session_id, client_id: command[:client_id])
|
|
602
612
|
|
|
@@ -952,7 +962,9 @@ module Samagotchi
|
|
|
952
962
|
"port" => @port,
|
|
953
963
|
"bind" => @bind,
|
|
954
964
|
"session_id" => @session_id,
|
|
955
|
-
"started_at" => Time.now.iso8601(3)
|
|
965
|
+
"started_at" => Time.now.iso8601(3),
|
|
966
|
+
# The chi this worker runs (chi update reports older ones).
|
|
967
|
+
"version" => Samagotchi::VERSION
|
|
956
968
|
}
|
|
957
969
|
record["input_format"] = @input_format if @input_format
|
|
958
970
|
path = File.join(session_dir, SIDECAR_FILE)
|
|
@@ -162,9 +162,13 @@ module Samagotchi
|
|
|
162
162
|
# !rollback, !cmd, /continue) for the worker to run. 202 = queued (body
|
|
163
163
|
# carries the command_id its :command_ran will name), 400 = not a
|
|
164
164
|
# command, 404 = a worker older than the route, 408 = too late.
|
|
165
|
+
# @param card [Boolean] the line is a card's action: its command_queued
|
|
166
|
+
# and command_ran carry card: true, and the UIs show no echo of it
|
|
165
167
|
# @return [Response]
|
|
166
|
-
def post_command(line:, client_id: nil)
|
|
167
|
-
|
|
168
|
+
def post_command(line:, client_id: nil, card: false)
|
|
169
|
+
payload = { line: line, client_id: client_id, deadline: deadline }
|
|
170
|
+
payload[:card] = true if card
|
|
171
|
+
post("command", payload, read_body: true)
|
|
168
172
|
end
|
|
169
173
|
|
|
170
174
|
# POST /session/:id/exit: ask the worker to exit now. 200 = it will
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: check-in
|
|
3
|
+
version: 0.1.1
|
|
4
|
+
scope: system
|
|
5
|
+
description: "Checks on a long turn: after N tool calls with no answer, a card asks the user (Nudge / Keep going / Stop), or the model is nudged to say what it has found"
|
|
6
|
+
trust_level: reviewed
|
|
7
|
+
plugin:
|
|
8
|
+
file: plugin.rb
|
|
9
|
+
sha256: sha256:b2ca10c949734ef3d31df9ff775cf136d7f86f185e875a4c16259e8f871bfb82
|
|
10
|
+
requires_chi: ">= 0.3.0"
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# The check-in bundle (docs/plugins.md, The check-in bundle): a turn that
|
|
2
|
+
# has made many tool calls without answering gets checked on. After `after`
|
|
3
|
+
# tool calls (then every `every` more) it asks the user with a card (Nudge /
|
|
4
|
+
# Keep going / Stop), nudges the model by itself, or only says so.
|
|
5
|
+
#
|
|
6
|
+
# A nudge is ctx.steer: the message joins the running turn at its next
|
|
7
|
+
# boundary as its own user message, so the model reads it before its next
|
|
8
|
+
# step. It never starts a turn, and one that arrives after the model's
|
|
9
|
+
# final answer is dropped: the card's Nudge then says so at the turn's end
|
|
10
|
+
# (after_turn's messages have no steer with its text).
|
|
11
|
+
#
|
|
12
|
+
# The count is per turn: before_turn resets it (and closes a card left open
|
|
13
|
+
# by a turn that failed or was interrupted, where after_turn doesn't fire);
|
|
14
|
+
# steering merged mid-turn does not. The /checkin commands are anytime ones
|
|
15
|
+
# (a card's actions run while the turn goes on), so the state is behind a
|
|
16
|
+
# Mutex.
|
|
17
|
+
#
|
|
18
|
+
# Settings (config.yml, bundles: check-in:):
|
|
19
|
+
# after: 50 tool calls in one turn before the first check-in
|
|
20
|
+
# every: 50 then again every this many more
|
|
21
|
+
# mode: ask ask | nudge | notify
|
|
22
|
+
# message: "..." what a nudge says; {calls} is the count
|
|
23
|
+
# ignore_tools: [task_wait, task_get, delegate_result]
|
|
24
|
+
require "securerandom"
|
|
25
|
+
|
|
26
|
+
class Plugin
|
|
27
|
+
MODES = %w[ask nudge notify].freeze
|
|
28
|
+
DEFAULT_MESSAGE = "You've made {calls} tool calls in this turn without answering. Say briefly what you've found " \
|
|
29
|
+
"so far and what's left, then answer now or continue."
|
|
30
|
+
DEFAULT_IGNORE = %w[task_wait task_get delegate_result].freeze
|
|
31
|
+
LAST_TOOLS = 5
|
|
32
|
+
USAGE = "usage: /checkin [on|off|<calls>|mode ask|nudge|notify|nudge|later|stop]"
|
|
33
|
+
|
|
34
|
+
def initialize(settings = {})
|
|
35
|
+
settings = {} unless settings.is_a?(Hash)
|
|
36
|
+
@after = positive(settings["after"]) || 50
|
|
37
|
+
@every = positive(settings["every"]) || @after
|
|
38
|
+
@mode = MODES.include?(settings["mode"].to_s) ? settings["mode"].to_s : "ask"
|
|
39
|
+
message = settings["message"].to_s.strip
|
|
40
|
+
@message = message.empty? ? DEFAULT_MESSAGE : message
|
|
41
|
+
@ignore = settings.key?("ignore_tools") ? Array(settings["ignore_tools"]).map(&:to_s) : DEFAULT_IGNORE
|
|
42
|
+
@enabled = true
|
|
43
|
+
@mutex = Mutex.new
|
|
44
|
+
reset
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def register(chi)
|
|
48
|
+
chi.on(:before_turn) { |_event, ctx| before_turn(ctx) }
|
|
49
|
+
chi.on(:after_tool_call) { |event, ctx| after_tool_call(event, ctx) }
|
|
50
|
+
chi.on(:after_turn) { |event, ctx| after_turn(event, ctx) }
|
|
51
|
+
chi.command "/checkin", "check on a long turn: status, on|off, <calls>, mode ask|nudge|notify, nudge, later, stop",
|
|
52
|
+
anytime: true do |args, ctx|
|
|
53
|
+
command(args.to_s.strip.downcase, ctx)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# --- the turn --------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def reset
|
|
62
|
+
@count = 0
|
|
63
|
+
@next_at = @after
|
|
64
|
+
@started = monotonic
|
|
65
|
+
@tools = []
|
|
66
|
+
@card_id = nil # this turn's card, once shown
|
|
67
|
+
@card_open = false
|
|
68
|
+
@nudge = nil # the card's Nudge this turn: { text:, count: }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def before_turn(ctx)
|
|
72
|
+
close_card(ctx, "The turn ended after #{calls} tool calls.")
|
|
73
|
+
@mutex.synchronize { reset }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def after_tool_call(event, ctx)
|
|
77
|
+
tool = event[:tool].to_s
|
|
78
|
+
return if @ignore.include?(tool)
|
|
79
|
+
|
|
80
|
+
due = @mutex.synchronize do
|
|
81
|
+
@count += 1
|
|
82
|
+
@tools = (@tools + [tool]).last(LAST_TOOLS)
|
|
83
|
+
next nil unless @enabled && @count >= @next_at
|
|
84
|
+
|
|
85
|
+
@next_at = @count + @every
|
|
86
|
+
[@count, @mode]
|
|
87
|
+
end
|
|
88
|
+
check_in(event, ctx, *due) if due
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def check_in(event, ctx, count, mode)
|
|
92
|
+
case mode
|
|
93
|
+
when "nudge"
|
|
94
|
+
ctx.notify("nudged the model after #{count} tool calls") if event[:steer]&.call(message(count))
|
|
95
|
+
when "notify"
|
|
96
|
+
ctx.notify("#{count} tool calls in this turn, no answer yet")
|
|
97
|
+
else
|
|
98
|
+
show_card(ctx, count)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def after_turn(event, ctx)
|
|
103
|
+
return if close_card(ctx, "The turn ended after #{calls} tool calls.")
|
|
104
|
+
|
|
105
|
+
nudge_not_sent(event, ctx)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The card said "Nudged": if the turn ended before the nudge joined it
|
|
109
|
+
# (the model answered first, or the turn was stopped), the card says so.
|
|
110
|
+
def nudge_not_sent(event, ctx)
|
|
111
|
+
id, nudge = @mutex.synchronize { [@card_id, @nudge] }
|
|
112
|
+
return unless id && nudge
|
|
113
|
+
return if steered?(event[:messages], nudge[:text])
|
|
114
|
+
|
|
115
|
+
first = event[:status].to_s == "canceled" ? "The turn ended first" : "The answer came first"
|
|
116
|
+
ctx.card(id: id, title: "check-in", body: "#{first}; nudge not sent.")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Whether this turn's messages (after its last prompt) have the steer.
|
|
120
|
+
def steered?(messages, text)
|
|
121
|
+
Array(messages).reverse_each do |message|
|
|
122
|
+
next unless message.is_a?(Hash)
|
|
123
|
+
|
|
124
|
+
role = (message[:role] || message["role"]).to_s
|
|
125
|
+
kind = (message[:kind] || message["kind"]).to_s
|
|
126
|
+
return true if kind == "steer" && (message[:content] || message["content"]).to_s.strip == text.strip
|
|
127
|
+
return false if role == "user" && kind != "steer"
|
|
128
|
+
end
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# One card per turn: a later check-in updates it in place.
|
|
133
|
+
def show_card(ctx, count)
|
|
134
|
+
id, body = @mutex.synchronize do
|
|
135
|
+
@card_id ||= "check-in-#{SecureRandom.hex(4)}"
|
|
136
|
+
@card_open = true
|
|
137
|
+
[@card_id, "#{elapsed} in this turn. Last tools: #{@tools.join(", ")}."]
|
|
138
|
+
end
|
|
139
|
+
ctx.card(id: id, title: "#{count} tool calls, no answer yet", body: body,
|
|
140
|
+
actions: [{ label: "Nudge", command: "/checkin nudge" },
|
|
141
|
+
{ label: "Keep going", command: "/checkin later" },
|
|
142
|
+
{ label: "Stop", command: "/checkin stop" }])
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The open card, replaced by one without actions (stale buttons go).
|
|
146
|
+
# @return [Boolean] whether there was one
|
|
147
|
+
def close_card(ctx, body)
|
|
148
|
+
id = @mutex.synchronize do
|
|
149
|
+
next nil unless @card_open
|
|
150
|
+
|
|
151
|
+
@card_open = false
|
|
152
|
+
@card_id
|
|
153
|
+
end
|
|
154
|
+
return false unless id
|
|
155
|
+
|
|
156
|
+
ctx.card(id: id, title: "check-in", body: body)
|
|
157
|
+
true
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# --- /checkin --------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def command(args, ctx)
|
|
163
|
+
case args
|
|
164
|
+
when "" then status
|
|
165
|
+
when "on", "off"
|
|
166
|
+
@mutex.synchronize { @enabled = args == "on" }
|
|
167
|
+
"check-in is #{args} for this session"
|
|
168
|
+
when /\A\d+\z/
|
|
169
|
+
threshold(Integer(args))
|
|
170
|
+
when /\Amode\s+(\S+)\z/
|
|
171
|
+
mode = Regexp.last_match(1)
|
|
172
|
+
return "check-in: unknown mode #{mode} (ask, nudge or notify)" unless MODES.include?(mode)
|
|
173
|
+
|
|
174
|
+
@mutex.synchronize { @mode = mode }
|
|
175
|
+
"check-in mode is #{mode} for this session"
|
|
176
|
+
when "nudge" then nudge(ctx)
|
|
177
|
+
when "later" then later(ctx)
|
|
178
|
+
when "stop" then stop(ctx)
|
|
179
|
+
else USAGE
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def status
|
|
184
|
+
@mutex.synchronize do
|
|
185
|
+
state = @enabled ? "on" : "off"
|
|
186
|
+
"check-in is #{state}: mode #{@mode}, after #{@after} tool calls, then every #{@every}; " \
|
|
187
|
+
"this turn: #{@count} tool calls"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# For this session: check in at +n+ calls and every +n+ after; a turn
|
|
192
|
+
# already past it is checked on +n+ calls from now.
|
|
193
|
+
def threshold(n)
|
|
194
|
+
return "check-in: the threshold must be at least 1" unless n.positive?
|
|
195
|
+
|
|
196
|
+
@mutex.synchronize do
|
|
197
|
+
@after = @every = n
|
|
198
|
+
@next_at = n > @count ? n : @count + n
|
|
199
|
+
end
|
|
200
|
+
"check-in after #{n} tool calls, then every #{n}, for this session"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def nudge(ctx)
|
|
204
|
+
count = calls
|
|
205
|
+
text = message(count)
|
|
206
|
+
return "check-in: no turn running" unless ctx.steer(text)
|
|
207
|
+
|
|
208
|
+
@mutex.synchronize { @nudge = { text: text, count: count } }
|
|
209
|
+
close_card(ctx, "Nudged the model at #{count} tool calls.")
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def later(ctx)
|
|
214
|
+
at = @mutex.synchronize { @next_at }
|
|
215
|
+
return "check-in: no check-in open" unless close_card(ctx, "Kept going; the next check-in is at #{at} tool calls.")
|
|
216
|
+
|
|
217
|
+
nil
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def stop(ctx)
|
|
221
|
+
return "check-in: no turn running" unless ctx.stop_turn("stopped from check-in")
|
|
222
|
+
|
|
223
|
+
close_card(ctx, "Stopped the turn at #{calls} tool calls.")
|
|
224
|
+
nil
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# --- helpers ---------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def calls = @mutex.synchronize { @count }
|
|
230
|
+
|
|
231
|
+
def message(count) = @message.gsub("{calls}", count.to_s)
|
|
232
|
+
|
|
233
|
+
def elapsed
|
|
234
|
+
seconds = (monotonic - @started).round
|
|
235
|
+
seconds < 60 ? "#{seconds}s" : "#{seconds / 60}m #{seconds % 60}s"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
239
|
+
|
|
240
|
+
def positive(value)
|
|
241
|
+
number = Integer(value.to_s, exception: false)
|
|
242
|
+
number&.positive? ? number : nil
|
|
243
|
+
end
|
|
244
|
+
end
|