samagotchi 0.138.0 → 0.139.1
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 +44 -1
- data/docs/architecture.md +33 -7
- data/docs/cli.md +6 -2
- data/docs/configuration.md +60 -5
- data/docs/internals/background-tasks.md +3 -3
- data/docs/internals/engine.md +2 -1
- data/docs/internals/tool-guardrails.md +2 -2
- data/docs/internals/web-push.md +20 -0
- data/docs/sessions.md +5 -3
- data/lib/samagotchi/bundle_command.rb +21 -21
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +8 -3
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +2 -2
- data/lib/samagotchi/cancellation_controller.rb +4 -4
- data/lib/samagotchi/config.rb +4 -0
- data/lib/samagotchi/context_fetch.rb +2 -2
- data/lib/samagotchi/cut.rb +21 -0
- data/lib/samagotchi/cut_policy.rb +6 -5
- data/lib/samagotchi/engine.rb +4 -2
- data/lib/samagotchi/engine_peers.rb +2 -1
- data/lib/samagotchi/hook_routes.rb +3 -2
- data/lib/samagotchi/kernel_loop.rb +8 -6
- data/lib/samagotchi/launch_mode.rb +5 -2
- data/lib/samagotchi/llm/chat_loop.rb +3 -2
- data/lib/samagotchi/llm/openai_chat.rb +1 -1
- data/lib/samagotchi/memory_bundle/status.rb +13 -3
- data/lib/samagotchi/project_env.rb +16 -2
- data/lib/samagotchi/reply_wait/poll.rb +9 -2
- data/lib/samagotchi/reply_wait.rb +9 -3
- data/lib/samagotchi/steer_cut.rb +2 -1
- data/lib/samagotchi/task_finish_note.rb +8 -5
- data/lib/samagotchi/task_watch.rb +11 -6
- data/lib/samagotchi/testing/boundary.rb +3 -1
- data/lib/samagotchi/tool_activity.rb +3 -3
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tools/builtins.rb +2 -1
- data/lib/samagotchi/tools/delegate_wait.rb +23 -2
- data/lib/samagotchi/tools/execute.rb +51 -9
- data/lib/samagotchi/tools/task_runtime.rb +1 -1
- data/lib/samagotchi/turn_note.rb +6 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +30 -158
- data/lib/samagotchi/web/auto_update.rb +282 -0
- data/lib/samagotchi/web/public/app.js +6 -4
- data/lib/samagotchi/web/public/composer.css +1 -1
- data/lib/samagotchi/web/public/index.html +10 -9
- data/lib/samagotchi/web/public/notifications.js +10 -1
- data/lib/samagotchi/web/public/revise_stack.js +6 -5
- data/lib/samagotchi/web/public/task_stop.js +3 -3
- data/lib/samagotchi/web/public/update_notice.js +10 -4
- data/lib/samagotchi/web/public/update_sheet.js +3 -2
- data/lib/samagotchi/web/push/notifier.rb +66 -5
- data/lib/samagotchi/web/server.rb +45 -1
- data/lib/samagotchi/web/session_access_routes.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +20 -3
- data/lib/samagotchi/web/update_check.rb +56 -10
- data/lib/samagotchi/web/update_ops.rb +240 -0
- data/lib/samagotchi/worker_task_wakes.rb +1 -1
- metadata +4 -1
|
@@ -454,10 +454,15 @@ class Plugin
|
|
|
454
454
|
client
|
|
455
455
|
end
|
|
456
456
|
|
|
457
|
-
# A server's env: the session's project env
|
|
458
|
-
#
|
|
457
|
+
# A server's env: the session's project env as execute's commands get it
|
|
458
|
+
# (the worker's mise env without chi's own Ruby and Bundler setup, on a
|
|
459
|
+
# chi that has it: Samagotchi::ProjectEnv.spawn_env) under the server's
|
|
460
|
+
# own keys. An older chi has only the overlay, or none.
|
|
459
461
|
def server_env(server)
|
|
460
|
-
project = defined?(Samagotchi::ProjectEnv)
|
|
462
|
+
project = if !defined?(Samagotchi::ProjectEnv) then {}
|
|
463
|
+
elsif Samagotchi::ProjectEnv.respond_to?(:spawn_env) then Samagotchi::ProjectEnv.spawn_env
|
|
464
|
+
else Samagotchi::ProjectEnv.current.env
|
|
465
|
+
end
|
|
461
466
|
project.merge(server.env)
|
|
462
467
|
end
|
|
463
468
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.139.1
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
|
-
self_map.md: sha256:
|
|
8
|
+
self_map.md: sha256:3253cb9ae7f09821d8fe1fed5154dd2b5e33fe0e9f096eee2af981d7d7d66f16
|
|
9
9
|
config_modification_protocol.md: sha256:ff4efccf2bdde453fc4e6741df8c18f78da78e353f6b1a5a64d0e7a631aea258
|
|
10
10
|
memory_guide.md: sha256:3b9ff779ee962f1307d56937c826d5e581923114f3f39455b779ca618b363cd7
|
|
11
11
|
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
asking for changes, checks failing, the PR closed), chi may start a turn for it with no user
|
|
32
32
|
message ("context <name> changed"): I tell my user what changed and act on nothing.
|
|
33
33
|
- `[TASK FINISHED from task <id8>, …]` is a background task I started (`task_create`, an `execute`
|
|
34
|
-
handed over at its limit) that finished while I wasn't
|
|
35
|
-
and log tail. When it opens a turn with no user message ("task <id8> finished"), I carry on with
|
|
34
|
+
handed over at its limit, or early when a message came for my turn) that finished while I wasn't
|
|
35
|
+
waiting on it: its exit code, duration and log tail. When it opens a turn with no user message ("task <id8> finished"), I carry on with
|
|
36
36
|
what I planned for its result, or tell my user in a sentence or two how it went. Without the
|
|
37
37
|
"chi started this turn" line it is only information for my next answer.
|
|
38
38
|
- `forget_outputs` (only when my model's `llm_context.strategy` has the experimental forget layer)
|
|
@@ -20,8 +20,8 @@ module Samagotchi
|
|
|
20
20
|
@generation = nil
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# @param detail [Object, nil] what the canceller says about it (
|
|
24
|
-
# generation
|
|
23
|
+
# @param detail [Object, nil] what the canceller says about it (the Cut
|
|
24
|
+
# of a cut generation, or a turn stopped by a hook)
|
|
25
25
|
def cancel!(reason = :manual, detail = nil)
|
|
26
26
|
listeners = []
|
|
27
27
|
@mutex.synchronize do
|
|
@@ -54,11 +54,11 @@ module Samagotchi
|
|
|
54
54
|
@mutex.synchronize { @detail }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
# Who stopped it when a hook did: the
|
|
57
|
+
# Who stopped it when a hook did: the Cut's +by+ (the bundle, or the
|
|
58
58
|
# hook's label); nil for any other cancel.
|
|
59
59
|
def stopped_by
|
|
60
60
|
reason, detail = @mutex.synchronize { [@reason, @detail] }
|
|
61
|
-
by = detail
|
|
61
|
+
by = detail.by.to_s if reason == :hook && detail.respond_to?(:by)
|
|
62
62
|
by unless by.nil? || by.empty?
|
|
63
63
|
end
|
|
64
64
|
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -293,6 +293,10 @@ module Samagotchi
|
|
|
293
293
|
Entry.new(key: "update.bundles", yaml_path: %w[update bundles], type: :bool, default: true, expose: %i[env config], lifecycle: :next_update),
|
|
294
294
|
Entry.new(key: "update.desktop", yaml_path: %w[update desktop], type: :bool, default: true, expose: %i[env config], lifecycle: :next_update),
|
|
295
295
|
Entry.new(key: "update.notify", yaml_path: %w[update notify], type: :enum, default: "on", expose: %i[env config], enum_values: %w[on off], lifecycle: :web_start),
|
|
296
|
+
# Unattended installs (chi web, Web::AutoUpdate): install: the release the checker found, once no turn runs anywhere;
|
|
297
|
+
# update.restart then says whether idle workers and chi web restart onto it (idle) or it waits for the owner (never).
|
|
298
|
+
Entry.new(key: "update.auto", yaml_path: %w[update auto], type: :enum, default: "off", expose: %i[env config], enum_values: %w[off install], lifecycle: :web_start),
|
|
299
|
+
Entry.new(key: "update.restart", yaml_path: %w[update restart], type: :enum, default: "idle", expose: %i[env config], enum_values: %w[idle never], lifecycle: :web_start),
|
|
296
300
|
Entry.new(key: "update.check_hours", yaml_path: %w[update check_hours], type: :integer, default: 24, expose: %i[env config], lifecycle: :web_start,
|
|
297
301
|
check: ->(value) { "at least 1" unless value >= 1 }),
|
|
298
302
|
Entry.new(key: "update.min_age_hours", yaml_path: %w[update min_age_hours], type: :integer, default: 24, expose: %i[env config], lifecycle: :web_start,
|
|
@@ -109,8 +109,8 @@ module Samagotchi
|
|
|
109
109
|
|
|
110
110
|
def env_for(location, name)
|
|
111
111
|
previous = location.snapshot_path(name)
|
|
112
|
-
# The session's project env (ProjectEnv) under the source's own keys.
|
|
113
|
-
ProjectEnv.
|
|
112
|
+
# The session's project env (ProjectEnv.spawn_env) under the source's own keys.
|
|
113
|
+
ProjectEnv.spawn_env.merge("SAMAGOTCHI_CONTEXT_NAME" => name,
|
|
114
114
|
"SAMAGOTCHI_CONTEXT_PREVIOUS" => File.exist?(previous) ? previous : nil)
|
|
115
115
|
end
|
|
116
116
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# Why a generation was cut: what CancellationController#cancel_generation!
|
|
5
|
+
# is given (a bundle's stop_generation, or Engine#cut_for_steer) and what
|
|
6
|
+
# the loops keep on a cut generation. +by+ is who stopped it (the hook's
|
|
7
|
+
# label, or "steer"), +reason+ what it said, +steer+ true when a queued user
|
|
8
|
+
# message cut it (no nudge, no retry attempt spent), +source+ that message,
|
|
9
|
+
# for the steer_cut row. The same value rides a whole-turn stop
|
|
10
|
+
# (stop_turn): the cancel note reads its +by+ and +reason+.
|
|
11
|
+
Cut = Data.define(:by, :reason, :steer, :source) do
|
|
12
|
+
def initialize(by: nil, reason: nil, steer: false, source: nil) = super
|
|
13
|
+
|
|
14
|
+
# A hook's cut (stop_generation, or stop_turn): the bundle +by+ and its
|
|
15
|
+
# +reason+.
|
|
16
|
+
def self.hook(by: nil, reason: nil) = new(by: by, reason: reason)
|
|
17
|
+
|
|
18
|
+
# Engine#cut_for_steer: a queued message cut the streaming generation.
|
|
19
|
+
def self.steer(source) = new(by: "steer", reason: "a new message", steer: true, source: source)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "cut"
|
|
3
4
|
require_relative "turn_note"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
@@ -30,7 +31,7 @@ module Samagotchi
|
|
|
30
31
|
|
|
31
32
|
module_function
|
|
32
33
|
|
|
33
|
-
# +cut+: the
|
|
34
|
+
# +cut+: the Cut (what stop_generation passed). +emit+ takes an
|
|
34
35
|
# event Hash; +inject+ puts queued input in (true when there was any).
|
|
35
36
|
# +finish_reason+ and +thinking_chars+: the cut generation's, for the
|
|
36
37
|
# retry's row.
|
|
@@ -40,13 +41,13 @@ module Samagotchi
|
|
|
40
41
|
return STOPPED if cancel_controller.cancelled?
|
|
41
42
|
|
|
42
43
|
# The row under the cut step, above the message it was cut for.
|
|
43
|
-
emit.call({ type: :steer_cut, iteration: iteration, source: cut
|
|
44
|
-
return AGAIN if inject.call || cut
|
|
44
|
+
emit.call({ type: :steer_cut, iteration: iteration, source: cut.source.to_s }) if cut.steer
|
|
45
|
+
return AGAIN if inject.call || cut.steer
|
|
45
46
|
|
|
46
47
|
if empty_retry.left?
|
|
47
|
-
empty_retry.nudge!(conversation, TurnNote.cut_retry(cut
|
|
48
|
+
empty_retry.nudge!(conversation, TurnNote.cut_retry(cut.by, cut.reason),
|
|
48
49
|
emit: emit, iteration: iteration, finish_reason: finish_reason,
|
|
49
|
-
thinking_chars: thinking_chars, stopped_by: cut
|
|
50
|
+
thinking_chars: thinking_chars, stopped_by: cut.by)
|
|
50
51
|
return AGAIN
|
|
51
52
|
end
|
|
52
53
|
empty_retry.drop_nudge!(conversation)
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -671,7 +671,8 @@ module Samagotchi
|
|
|
671
671
|
end
|
|
672
672
|
|
|
673
673
|
# Whether a message a person sent waits for the running turn's next
|
|
674
|
-
# boundary (run_turn's input_waiting: probe): a task_wait
|
|
674
|
+
# boundary (run_turn's input_waiting: probe): a task_wait or delegate wait
|
|
675
|
+
# returns on it, an execute hands its command over early
|
|
675
676
|
# (PeerView#input_waiting?). False outside a turn, for a turn given no
|
|
676
677
|
# probe, and when the probe raises (warned once a turn; the tool never
|
|
677
678
|
# fails for it).
|
|
@@ -2242,7 +2243,8 @@ module Samagotchi
|
|
|
2242
2243
|
# by the agentic loop at iteration boundaries (see KernelLoop#run).
|
|
2243
2244
|
# @param input_waiting [#call, nil] optional probe: true while a message
|
|
2244
2245
|
# a person sent waits for the next boundary (it peeks, never drains);
|
|
2245
|
-
# a running task_wait returns on it
|
|
2246
|
+
# a running task_wait or delegate wait returns on it, a running execute
|
|
2247
|
+
# hands its command over (#input_waiting?). nil: never.
|
|
2246
2248
|
# @param continue [Boolean] resume the conversation without appending a
|
|
2247
2249
|
# user prompt (continue after the iteration limit, reminder turns);
|
|
2248
2250
|
# +prompt+ is ignored and :turn_started carries `continue: true`
|
|
@@ -9,7 +9,8 @@ module Samagotchi
|
|
|
9
9
|
# is the running turn's cancel, for a tool that waits (delegate_result):
|
|
10
10
|
# the controller is set from another thread and a tool gets no other
|
|
11
11
|
# way to see it. input_waiting? is whether a person's message waits for
|
|
12
|
-
# the running turn's next boundary (task_wait
|
|
12
|
+
# the running turn's next boundary (task_wait and the delegate tools' waits
|
|
13
|
+
# return on it, execute hands its command over early).
|
|
13
14
|
PeerView = Struct.new(:engine) do
|
|
14
15
|
def session_id = engine.session&.id
|
|
15
16
|
def cwd = engine.session&.working_directory
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "cut"
|
|
3
4
|
require_relative "event_routes"
|
|
4
5
|
require_relative "hooks"
|
|
5
6
|
require_relative "log"
|
|
@@ -104,7 +105,7 @@ module Samagotchi
|
|
|
104
105
|
return false unless ctrl && !ctrl.cancelled?
|
|
105
106
|
|
|
106
107
|
notify("stopped the turn: #{reason}", :warn, hook)
|
|
107
|
-
ctrl.cancel!(:hook,
|
|
108
|
+
ctrl.cancel!(:hook, Cut.hook(by: source(hook), reason: reason.to_s))
|
|
108
109
|
end
|
|
109
110
|
|
|
110
111
|
# Cut the streaming generation (reason :hook); the turn goes on and asks
|
|
@@ -115,7 +116,7 @@ module Samagotchi
|
|
|
115
116
|
ctrl = @cancel_controller.call
|
|
116
117
|
return false unless ctrl
|
|
117
118
|
|
|
118
|
-
ctrl.cancel_generation!(:hook,
|
|
119
|
+
ctrl.cancel_generation!(:hook, Cut.hook(by: source(hook), reason: reason.to_s))
|
|
119
120
|
end
|
|
120
121
|
|
|
121
122
|
private
|
|
@@ -13,6 +13,7 @@ require_relative "client"
|
|
|
13
13
|
require_relative "llm/errors"
|
|
14
14
|
require_relative "log"
|
|
15
15
|
require_relative "empty_answer_retry"
|
|
16
|
+
require_relative "cut"
|
|
16
17
|
require_relative "cut_policy"
|
|
17
18
|
require_relative "thinking"
|
|
18
19
|
require_relative "hooks"
|
|
@@ -327,8 +328,9 @@ module Samagotchi
|
|
|
327
328
|
# One request: the prompt and its images as sent, the images' token
|
|
328
329
|
# estimate, and the window it was measured against.
|
|
329
330
|
Request = Struct.new(:prompt, :images, :image_tokens, :window, :prefill, keyword_init: true)
|
|
330
|
-
# One generation: the response (nil when cut), the
|
|
331
|
-
#
|
|
331
|
+
# One generation: the response (nil when cut), the Cut that stopped it
|
|
332
|
+
# (an empty one when nothing said why), this generation's own server
|
|
333
|
+
# counts, the model the server named, the
|
|
332
334
|
# thinking it streamed, and why it stopped (llama.cpp's finish
|
|
333
335
|
# reason, Client.finish_reason_from; nil when not named),
|
|
334
336
|
# and its prompt-cache counts (the largest seen: they come cumulative),
|
|
@@ -472,7 +474,7 @@ module Samagotchi
|
|
|
472
474
|
rescue Client::RequestCancelled
|
|
473
475
|
raise unless cut?(generation_controller, turn.cancel_controller)
|
|
474
476
|
|
|
475
|
-
generation.cut = generation_controller.detail ||
|
|
477
|
+
generation.cut = generation_controller.detail || Cut.new
|
|
476
478
|
nil
|
|
477
479
|
end
|
|
478
480
|
generation.cut ? cut_generation(turn, generation, buffer_mark) : completed_generation(turn, request, generation)
|
|
@@ -634,8 +636,8 @@ module Samagotchi
|
|
|
634
636
|
turn.buffer.slice!(buffer_mark..)
|
|
635
637
|
emit(turn, type: :generation_completed, iteration: turn.iteration, content_length: 0,
|
|
636
638
|
thinking_chars: generation.streamed_thinking, served_model: generation.served_model,
|
|
637
|
-
requested_model: turn.model_name, finish_reason: "stopped", stopped_by: generation.cut
|
|
638
|
-
stop_reason: generation.cut
|
|
639
|
+
requested_model: turn.model_name, finish_reason: "stopped", stopped_by: generation.cut.by,
|
|
640
|
+
stop_reason: generation.cut.reason)
|
|
639
641
|
end
|
|
640
642
|
|
|
641
643
|
def completed_generation(turn, request, generation)
|
|
@@ -668,7 +670,7 @@ module Samagotchi
|
|
|
668
670
|
raise Client::RequestCancelled, turn.cancel_controller.reason if outcome.stopped?
|
|
669
671
|
return :next if outcome.again?
|
|
670
672
|
|
|
671
|
-
emit(turn, type: :generation_cancelled, iteration: turn.iteration, reason: :hook, stopped_by: cut
|
|
673
|
+
emit(turn, type: :generation_cancelled, iteration: turn.iteration, reason: :hook, stopped_by: cut.by)
|
|
672
674
|
cancelled_result(turn.conversation, tool_activity: turn.tool_activity, reason: :hook, partial_assistant_text: "")
|
|
673
675
|
end
|
|
674
676
|
|
|
@@ -17,8 +17,11 @@ module Samagotchi
|
|
|
17
17
|
# into a kept one.
|
|
18
18
|
SCRATCH_LEFTOVER = "that's a leftover scratch session; it is deleted at the next sweep (chi sessions clean)"
|
|
19
19
|
# A worker spawned or resumed for an attach whose Bridge never came (%s:
|
|
20
|
-
# the session id).
|
|
21
|
-
|
|
20
|
+
# the session id). A worker whose chi home couldn't load chi logs
|
|
21
|
+
# SessionManager::HOME_BOOT_FAILED and exits: HomeDoctor's boot row says
|
|
22
|
+
# what and what to run, so the text names chi home doctor.
|
|
23
|
+
BRIDGE_LATE = "the worker for session %s did not start its Bridge in time " \
|
|
24
|
+
"(chi home doctor shows a worker that failed to boot)"
|
|
22
25
|
# An attach (or a resume) that found the session's worker leaving, its
|
|
23
26
|
# Bridge closed and its recap on leave still being written: it waits for
|
|
24
27
|
# the worker to let go (OwnerLock.await_release), then starts a new one.
|
|
@@ -14,6 +14,7 @@ require_relative "stream_drops"
|
|
|
14
14
|
require_relative "../kernel_loop"
|
|
15
15
|
require_relative "../answer_display"
|
|
16
16
|
require_relative "../answer_refs"
|
|
17
|
+
require_relative "../cut"
|
|
17
18
|
require_relative "../cut_policy"
|
|
18
19
|
require_relative "../context_status"
|
|
19
20
|
require_relative "../context_window"
|
|
@@ -763,7 +764,7 @@ module Samagotchi
|
|
|
763
764
|
rescue RequestCancelled
|
|
764
765
|
raise unless generation_controller&.cancelled? && !@cancel_controller.cancelled?
|
|
765
766
|
|
|
766
|
-
return cut_response(iteration, thought, generation_controller.detail ||
|
|
767
|
+
return cut_response(iteration, thought, generation_controller.detail || Cut.new)
|
|
767
768
|
end
|
|
768
769
|
dropped = settle_thinking_drop(iteration, response.thinking_drop)
|
|
769
770
|
record_context_status(response.usage, window)
|
|
@@ -820,7 +821,7 @@ module Samagotchi
|
|
|
820
821
|
# stream never sent its last chunk).
|
|
821
822
|
def cut_response(iteration, thought, cut)
|
|
822
823
|
emit(type: :generation_completed, iteration: iteration, content_length: 0, thinking_chars: thought.length,
|
|
823
|
-
requested_model: @model_name, finish_reason: "stopped", stopped_by: cut
|
|
824
|
+
requested_model: @model_name, finish_reason: "stopped", stopped_by: cut.by, stop_reason: cut.reason)
|
|
824
825
|
response = ChatResponse.new(text: "", reasoning: thought, tool_calls: [], usage: nil, finish_reason: "stopped",
|
|
825
826
|
cut: cut)
|
|
826
827
|
[response, nil]
|
|
@@ -26,7 +26,7 @@ module Samagotchi
|
|
|
26
26
|
# +provider+ is the provider that served it (the body's `provider`,
|
|
27
27
|
# OpenRouter's routing name); nil when the server says nothing.
|
|
28
28
|
# +cut+ is set on the empty response the chat loop makes for a
|
|
29
|
-
# generation a plugin cut (stop_generation):
|
|
29
|
+
# generation a plugin cut (stop_generation): the Cut.
|
|
30
30
|
# +top_logprobs+: the first answer token's likeliest alternatives
|
|
31
31
|
# (TokenLogprob) when the request asked for logprobs and the server
|
|
32
32
|
# sent them (a non-streamed answer only); [] otherwise.
|
|
@@ -12,6 +12,16 @@ require_relative "../model_overlay"
|
|
|
12
12
|
module Samagotchi
|
|
13
13
|
module MemoryBundle
|
|
14
14
|
module Status
|
|
15
|
+
# A bundle as this chi reads it now (what chi bundle status prints):
|
|
16
|
+
# its stored record, the scope it resolves to (scope_error when it
|
|
17
|
+
# can't), the folder its files belong in, what each file looks like on
|
|
18
|
+
# disk, its installed plugin, why none of its hooks load, and its needs
|
|
19
|
+
# with what this process's PATH found. The +files+ entries and +plugin+
|
|
20
|
+
# stay Hashes: their fields differ per case (an unchecked file has no
|
|
21
|
+
# paths at all).
|
|
22
|
+
Report = Data.define(:provenance, :scope, :scope_error, :target_dir, :files, :plugin,
|
|
23
|
+
:hooks_requires_failure, :needs)
|
|
24
|
+
|
|
15
25
|
# A scope this chi can't resolve (a newer chi's, a hand edit) isn't
|
|
16
26
|
# guessed: scope_error says so, target_dir is nil and each file is
|
|
17
27
|
# unchecked (no disk reads, nothing counted as missing).
|
|
@@ -50,9 +60,9 @@ module Samagotchi
|
|
|
50
60
|
base_path: base_path
|
|
51
61
|
}
|
|
52
62
|
end
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
Report.new(provenance: bundle, scope: scope, scope_error: target_dir ? nil : "unknown scope: #{scope}",
|
|
64
|
+
target_dir: target_dir, files: details, plugin: plugin_status(provenance, bundle),
|
|
65
|
+
hooks_requires_failure: hooks_requires_failure(bundle), needs: needs_status(bundle))
|
|
56
66
|
end
|
|
57
67
|
|
|
58
68
|
# Why none of the bundle's hooks load (this chi doesn't meet its
|
|
@@ -50,8 +50,10 @@ module Samagotchi
|
|
|
50
50
|
# Keys the tools' overlay never sets: a `chi` a tool runs (the shim's)
|
|
51
51
|
# reads its config, state and log dirs, its settings and the machine its
|
|
52
52
|
# session commands go to (CHI_REMOTE) from them, and the test-run flags.
|
|
53
|
-
#
|
|
54
|
-
|
|
53
|
+
# And TMPDIR: the guardrails judge a tool's `$TMPDIR/x` and its tmp dirs
|
|
54
|
+
# (Guardrails::PathResolve.tmp_roots) by the worker's own TMPDIR, so the
|
|
55
|
+
# tools must run on that one too. Dropped, with one warning.
|
|
56
|
+
RESERVED_KEYS = %w[HOME TMPDIR RACK_ENV CI CHI_REMOTE].freeze
|
|
55
57
|
RESERVED_PREFIXES = %w[XDG_ SAMAGOTCHI_].freeze
|
|
56
58
|
# Never set from an ORIG_ENV record (a tool can write it), only unset:
|
|
57
59
|
# loader and Bundler variables.
|
|
@@ -112,6 +114,18 @@ module Samagotchi
|
|
|
112
114
|
# @return [Hash{String=>String, nil}]
|
|
113
115
|
def tools_env = current.env.except(*TOOLS_DROP)
|
|
114
116
|
|
|
117
|
+
# The whole env change for a command chi starts for the session outside
|
|
118
|
+
# execute (MCP servers, context sources): what execute spawns over
|
|
119
|
+
# (Tools::TaskRuntime.sanitized_spawn_env: chi's own RUBYOPT without
|
|
120
|
+
# bundler/setup, no RUBYLIB or Bundler variables), then #tools_env. One
|
|
121
|
+
# env for every command, so a worker run from a checkout under `bundle
|
|
122
|
+
# exec` hands none of them its Gemfile.
|
|
123
|
+
# @return [Hash{String=>String, nil}]
|
|
124
|
+
def spawn_env
|
|
125
|
+
require_relative "tools/task_runtime" # lazily: bin/chi loads this file for #take_orig!
|
|
126
|
+
Tools::TaskRuntime.sanitized_spawn_env.merge(tools_env)
|
|
127
|
+
end
|
|
128
|
+
|
|
115
129
|
# Specs: back to NONE.
|
|
116
130
|
def reset! = @current = NONE
|
|
117
131
|
|
|
@@ -16,12 +16,13 @@ module Samagotchi
|
|
|
16
16
|
# them. ReplyWait.call's parameters say what each means.
|
|
17
17
|
class Poll
|
|
18
18
|
def initialize(id, state_dir:, cursor:, timeout:, poll_interval:, cancelled:, baseline:, owner_grace:, interject:,
|
|
19
|
-
turn:, queued:, ended_before:, running_look: nil)
|
|
19
|
+
turn:, queued:, ended_before:, running_look: nil, message_waiting: -> { false })
|
|
20
20
|
@id = id
|
|
21
21
|
@state_dir = state_dir
|
|
22
22
|
@cursor = cursor
|
|
23
23
|
@poll_interval = poll_interval
|
|
24
24
|
@cancelled = cancelled
|
|
25
|
+
@message_waiting = message_waiting
|
|
25
26
|
@baseline = baseline
|
|
26
27
|
@owner_grace = owner_grace
|
|
27
28
|
@interject = interject
|
|
@@ -80,7 +81,7 @@ module Samagotchi
|
|
|
80
81
|
follow_merge(session)
|
|
81
82
|
reply(session) || settled_turn(session) || status_end(session) || question(session) || held(session) || canceled ||
|
|
82
83
|
ended_before_reply(session) || ended_before_turn(session) || turn_end(session) || owner_gone(session) ||
|
|
83
|
-
timed_out
|
|
84
|
+
message_waiting || timed_out
|
|
84
85
|
end
|
|
85
86
|
|
|
86
87
|
def look_at_input
|
|
@@ -189,6 +190,12 @@ module Samagotchi
|
|
|
189
190
|
|
|
190
191
|
def canceled = @cancelled.call ? Result.new(status: :canceled) : nil
|
|
191
192
|
|
|
193
|
+
# A person's message waits for the caller's turn (call's
|
|
194
|
+
# message_waiting:): after every outcome the child can arrive at (a
|
|
195
|
+
# reply, a question, an end, a gone worker) and a cancel, before the
|
|
196
|
+
# deadline. The child isn't touched.
|
|
197
|
+
def message_waiting = @message_waiting.call ? Result.new(status: :message_waiting) : nil
|
|
198
|
+
|
|
192
199
|
# The turn that ended before this wait (ended_before: chi send --wait
|
|
193
200
|
# with no message, on a session that went idle in the gap between
|
|
194
201
|
# waits): its last_turn still stands, the session is idle, so its end
|
|
@@ -27,7 +27,8 @@ module Samagotchi
|
|
|
27
27
|
TURN_IDS_FEATURE = "turn_ids"
|
|
28
28
|
|
|
29
29
|
# @!attribute status [Symbol] :done, :waiting_for_answer, :no_reply,
|
|
30
|
-
# :error, :stopped, :canceled, :timeout or
|
|
30
|
+
# :error, :stopped, :canceled, :message_waiting, :timeout or
|
|
31
|
+
# :worker_gone
|
|
31
32
|
# @!attribute text [String, nil] the reply (:done) or the worker's error
|
|
32
33
|
# (:error)
|
|
33
34
|
# @!attribute file [String, nil] the reply's filename, the next cursor
|
|
@@ -159,14 +160,19 @@ module Samagotchi
|
|
|
159
160
|
# @param running_look [RunningLook, nil] the running turn an earlier
|
|
160
161
|
# call of the same wait saw (its :timeout result's running_look): its
|
|
161
162
|
# settle ends this call too, as if this call had seen it running
|
|
163
|
+
# @param message_waiting [#call] true once a person's message waits for
|
|
164
|
+
# the caller's turn (Engine#input_waiting?): ends the wait early
|
|
165
|
+
# (:message_waiting) when nothing else has, after a cancel, before the
|
|
166
|
+
# deadline. The session waited for is never touched.
|
|
162
167
|
# @return [Result]
|
|
163
168
|
# @raise [ArgumentError] no such session
|
|
164
169
|
def call(id, state_dir:, cursor:, timeout: nil, poll_interval: POLL_INTERVAL, cancelled: -> { false },
|
|
165
170
|
baseline: nil, owner_grace: nil, interject: nil, turn: nil, queued: false, ended_before: false,
|
|
166
|
-
running_look: nil)
|
|
171
|
+
running_look: nil, message_waiting: -> { false })
|
|
167
172
|
Poll.new(id, state_dir: state_dir, cursor: cursor, timeout: timeout, poll_interval: poll_interval,
|
|
168
173
|
cancelled: cancelled, baseline: baseline, owner_grace: owner_grace, interject: interject, turn: turn,
|
|
169
|
-
queued: queued, ended_before: ended_before, running_look: running_look
|
|
174
|
+
queued: queued, ended_before: ended_before, running_look: running_look,
|
|
175
|
+
message_waiting: message_waiting).run
|
|
170
176
|
end
|
|
171
177
|
|
|
172
178
|
# The session as it was before a message went in (call's baseline:):
|
data/lib/samagotchi/steer_cut.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "config"
|
|
4
|
+
require_relative "cut"
|
|
4
5
|
require_relative "log"
|
|
5
6
|
require_relative "steer"
|
|
6
7
|
require_relative "generation_phase"
|
|
@@ -131,7 +132,7 @@ module Samagotchi
|
|
|
131
132
|
|
|
132
133
|
def cut!(ctrl, source, waited: false)
|
|
133
134
|
age = @phase.age
|
|
134
|
-
cut = ctrl.cancel_generation!(:steer,
|
|
135
|
+
cut = ctrl.cancel_generation!(:steer, Cut.steer(source))
|
|
135
136
|
if cut
|
|
136
137
|
@waiting.clear!
|
|
137
138
|
fields = { source: source, age: age&.round(1) }
|
|
@@ -21,24 +21,27 @@ module Samagotchi
|
|
|
21
21
|
|
|
22
22
|
# @param record [Tools::TaskRecord] the finished task
|
|
23
23
|
# @param limit_sec [Integer, nil] the limit an execute handed it over at
|
|
24
|
+
# @param early_sec [Integer, nil] the seconds an execute had run when a
|
|
25
|
+
# message from the user handed it over before its limit
|
|
24
26
|
# @param wake [Boolean] the wake variant
|
|
25
27
|
# @param cap [Integer] max_tool_output_chars: task_wait's text is cut to
|
|
26
28
|
# it as ToolRunner cuts the tool's result
|
|
27
29
|
# @return [Hash] the note (ContextNote.message's keys)
|
|
28
|
-
def note(record, limit_sec:, wake:, cap:, turn_id: nil, now: Time.now)
|
|
30
|
+
def note(record, limit_sec:, wake:, cap:, early_sec: nil, turn_id: nil, now: Time.now)
|
|
29
31
|
note = { note_id: "task-#{record.id}", source: "task #{short_id(record.id)}", header: ContextNote::TASK_HEADER,
|
|
30
|
-
text: text(record, limit_sec: limit_sec, wake: wake, cap: cap), created_at: now.iso8601 }
|
|
32
|
+
text: text(record, limit_sec: limit_sec, early_sec: early_sec, wake: wake, cap: cap), created_at: now.iso8601 }
|
|
31
33
|
wake ? note.merge(turn_start: true, turn_id: turn_id) : note
|
|
32
34
|
end
|
|
33
35
|
|
|
34
|
-
def text(record, limit_sec:, wake:, cap:)
|
|
35
|
-
[headline(record, limit_sec), "command: #{record.command}", body(record, cap), (WAKE_TEXT if wake)].compact.join("\n")
|
|
36
|
+
def text(record, limit_sec:, wake:, cap:, early_sec: nil)
|
|
37
|
+
[headline(record, limit_sec, early_sec), "command: #{record.command}", body(record, cap), (WAKE_TEXT if wake)].compact.join("\n")
|
|
36
38
|
end
|
|
37
39
|
|
|
38
|
-
def headline(record, limit_sec)
|
|
40
|
+
def headline(record, limit_sec, early_sec = nil)
|
|
39
41
|
seconds = duration(record)
|
|
40
42
|
outcome = record.exit_code.nil? ? record.status.to_s : "exit #{record.exit_code}"
|
|
41
43
|
handed = " (handed over by execute at its #{limit_sec}s limit)" if limit_sec
|
|
44
|
+
handed ||= " (handed over by execute after #{early_sec}s: a message from the user was waiting)" if early_sec
|
|
42
45
|
"Background task #{short_id(record.id)} finished: #{outcome}#{" after #{duration_text(seconds)}" if seconds}#{handed}."
|
|
43
46
|
end
|
|
44
47
|
|
|
@@ -19,12 +19,14 @@ module Samagotchi
|
|
|
19
19
|
# and anytime commands run on threads of their own.
|
|
20
20
|
class TaskWatch
|
|
21
21
|
# A watched task: +limit_sec+, the limit an execute handed it over at
|
|
22
|
-
# (nil for task_create's); +
|
|
22
|
+
# (nil for task_create's); +early_sec+, the seconds an execute had run
|
|
23
|
+
# when a message from the user handed it over early (nil otherwise);
|
|
24
|
+
# +note_only+, the user stopped the turn that
|
|
23
25
|
# started or waited on it (M5): its finish is a plain note, no wake;
|
|
24
26
|
# +failures+, how often its note couldn't be written (#put_back) or the
|
|
25
27
|
# look for it failed (#look_failed).
|
|
26
|
-
Entry = Data.define(:id, :limit_sec, :note_only, :failures) do
|
|
27
|
-
def initialize(id:, limit_sec: nil, note_only: false, failures: 0) = super
|
|
28
|
+
Entry = Data.define(:id, :limit_sec, :early_sec, :note_only, :failures) do
|
|
29
|
+
def initialize(id:, limit_sec: nil, early_sec: nil, note_only: false, failures: 0) = super
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
# A task whose note or look failed this often still gets tried at each
|
|
@@ -41,6 +43,7 @@ module Samagotchi
|
|
|
41
43
|
TASK_LINE = /\A#{Tools::TaskRuntime::RAN_AS_LINE}(?:\[task_(?:wait|get|stop)\]\n)?task_id: (\S+)/
|
|
42
44
|
LOOK_TOOLS = %w[task_wait task_get task_stop].freeze
|
|
43
45
|
HANDED_OVER_LIMIT = /#{Regexp.escape(Tools::Execute::HANDED_OVER)} (\d+)s limit/
|
|
46
|
+
HANDED_OVER_EARLY = /#{Regexp.escape(Tools::Execute::HANDED_OVER)} (\d+)s mark/
|
|
44
47
|
|
|
45
48
|
MODE_KEY = "session.task_wakes"
|
|
46
49
|
MODES = %w[wake queue off].freeze
|
|
@@ -75,16 +78,18 @@ module Samagotchi
|
|
|
75
78
|
output = event[:output].to_s
|
|
76
79
|
output = output.scrub unless output.valid_encoding?
|
|
77
80
|
if (id = Tools::TaskRuntime.created_id(tool, output))
|
|
78
|
-
|
|
81
|
+
executed = tool == Tools::Execute::NAME
|
|
82
|
+
add(id, limit_sec: executed ? output[HANDED_OVER_LIMIT, 1]&.to_i : nil,
|
|
83
|
+
early_sec: executed ? output[HANDED_OVER_EARLY, 1]&.to_i : nil)
|
|
79
84
|
elsif LOOK_TOOLS.include?(tool) && (id = output[TASK_LINE, 1])
|
|
80
85
|
touched(id)
|
|
81
86
|
remove(id) if seen?(tool, output)
|
|
82
87
|
end
|
|
83
88
|
end
|
|
84
89
|
|
|
85
|
-
def add(id, limit_sec: nil)
|
|
90
|
+
def add(id, limit_sec: nil, early_sec: nil)
|
|
86
91
|
@mutex.synchronize do
|
|
87
|
-
@entries[id] = Entry.new(id: id, limit_sec: limit_sec)
|
|
92
|
+
@entries[id] = Entry.new(id: id, limit_sec: limit_sec, early_sec: early_sec)
|
|
88
93
|
@turn_ids << id
|
|
89
94
|
end
|
|
90
95
|
end
|
|
@@ -103,7 +103,9 @@ module Samagotchi
|
|
|
103
103
|
"Samagotchi::TokenUsage::CHARS_PER_TOKEN" => "estimates a tool's tokens: could be a stable constant",
|
|
104
104
|
'require "samagotchi/tool_declarations"' => "loaded for Api.tool_spec's schemas",
|
|
105
105
|
"Samagotchi::ProjectEnv" => "checks the worker has a project env (an older chi doesn't)",
|
|
106
|
-
"Samagotchi::ProjectEnv.current" => "starts a server in the session's project env: needs a ctx tools-env API"
|
|
106
|
+
"Samagotchi::ProjectEnv.current" => "starts a server in the session's project env: needs a ctx tools-env API",
|
|
107
|
+
"Samagotchi::ProjectEnv.respond_to?" => "checks the worker has ProjectEnv.spawn_env (an older chi doesn't)",
|
|
108
|
+
"Samagotchi::ProjectEnv.spawn_env" => "starts a server in the session's project env: needs a ctx tools-env API"
|
|
107
109
|
},
|
|
108
110
|
"slow-commands" => {
|
|
109
111
|
'require "samagotchi/guardrails/shell_lex"' => "for ShellLex",
|
|
@@ -264,8 +264,8 @@ module Samagotchi
|
|
|
264
264
|
"ok"
|
|
265
265
|
end
|
|
266
266
|
|
|
267
|
-
# An execute still running at its limit
|
|
268
|
-
# (Tools::Execute::HANDED_OVER).
|
|
267
|
+
# An execute still running at its limit (or when a message from the user
|
|
268
|
+
# waited), handed over as a task (Tools::Execute::HANDED_OVER).
|
|
269
269
|
def handed_over?(tool_name, text)
|
|
270
270
|
return false unless tool_name == Tools::Execute::NAME && text.to_s.start_with?(Tools::Execute::HANDED_OVER)
|
|
271
271
|
|
|
@@ -273,7 +273,7 @@ module Samagotchi
|
|
|
273
273
|
text.to_s.each_line.first.to_s.scrub.match?(HANDED_OVER_START)
|
|
274
274
|
end
|
|
275
275
|
|
|
276
|
-
HANDED_OVER_START = /\A#{Regexp.escape(Tools::Execute::HANDED_OVER)}
|
|
276
|
+
HANDED_OVER_START = /\A#{Regexp.escape(Tools::Execute::HANDED_OVER)} #{Tools::Execute::HANDED_OVER_AFTER}/
|
|
277
277
|
|
|
278
278
|
def execute_failed?(text)
|
|
279
279
|
code = execute_exit_code(text)
|
|
@@ -20,7 +20,7 @@ module Samagotchi
|
|
|
20
20
|
TOOL_SCHEMAS = [
|
|
21
21
|
{
|
|
22
22
|
name: "execute",
|
|
23
|
-
description: "Run any shell command in a working directory (defaults to the project root) and see stdout, stderr, and exit code. Large output may be truncated to a head+tail preview with metadata. A command still running at the time limit keeps running as a background task (by default): the result gives its task_id, and task_wait waits for it. Don't background with &; use task_create.",
|
|
23
|
+
description: "Run any shell command in a working directory (defaults to the project root) and see stdout, stderr, and exit code. Large output may be truncated to a head+tail preview with metadata. A command still running at the time limit, or after a few seconds when a message from the user is waiting, keeps running as a background task (by default): the result gives its task_id, and task_wait waits for it. Don't background with &; use task_create.",
|
|
24
24
|
parameters: {
|
|
25
25
|
type: "object",
|
|
26
26
|
properties: {
|
|
@@ -395,7 +395,7 @@ module Samagotchi
|
|
|
395
395
|
},
|
|
396
396
|
{
|
|
397
397
|
name: "delegate_result",
|
|
398
|
-
description: "Wait for a delegated child session's next reply, only when this turn can't go on without it: the child named, or this session's newest running child (name the child when a delegate report is about it). After delegate with wait false you don't need this: chi brings each child's reply by itself as a delegate report, also when you are idle, so end your turn or keep working instead of waiting here. A delegate report already carries the reply: don't call this for it. Returns early if the child waits for a question the user must answer. A tool call the child needs approved goes to your user while this waits; the result says how it was answered.",
|
|
398
|
+
description: "Wait for a delegated child session's next reply, only when this turn can't go on without it: the child named, or this session's newest running child (name the child when a delegate report is about it). After delegate with wait false you don't need this: chi brings each child's reply by itself as a delegate report, also when you are idle, so end your turn or keep working instead of waiting here. A delegate report already carries the reply: don't call this for it. Returns early if the child waits for a question the user must answer, or a message from the user arrives (the child keeps running). A tool call the child needs approved goes to your user while this waits; the result says how it was answered.",
|
|
399
399
|
parameters: {
|
|
400
400
|
type: "object",
|
|
401
401
|
properties: {
|
|
@@ -92,6 +92,7 @@ module Samagotchi
|
|
|
92
92
|
end,
|
|
93
93
|
Execute::NAME => lambda do |call, kctx|
|
|
94
94
|
Execute.call(call[:content], cwd: call[:cwd], cancelled: cancelled_proc(kctx), stop_reason: stop_reason_proc(kctx),
|
|
95
|
+
message_waiting: input_waiting_proc(kctx),
|
|
95
96
|
**participant_execute(kctx, project_env(kctx).merge(parent_env(kctx))))
|
|
96
97
|
end,
|
|
97
98
|
RegisterReminder::NAME => lambda do |call, kctx|
|
|
@@ -202,7 +203,7 @@ module Samagotchi
|
|
|
202
203
|
|
|
203
204
|
# A person's message waits for the turn's next boundary
|
|
204
205
|
# (Engine#input_waiting?, through its PeerView): a task_wait returns on
|
|
205
|
-
# it. A kernel whose peers can't tell: never.
|
|
206
|
+
# it, an execute hands its command over early. A kernel whose peers can't tell: never.
|
|
206
207
|
def input_waiting_proc(kctx)
|
|
207
208
|
peers = kctx.peers
|
|
208
209
|
-> { peers.respond_to?(:input_waiting?) && !!peers.input_waiting? }
|