samagotchi 0.90.0 → 0.92.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 +79 -1
- data/README.md +5 -2
- data/bin/chi +4 -4
- data/docs/architecture.md +17 -13
- data/docs/bundle-authoring.md +6 -11
- data/docs/cli.md +7 -4
- data/docs/configuration.md +8 -4
- data/docs/context.md +5 -3
- data/docs/hooks.md +3 -3
- data/docs/internals/background-tasks.md +27 -2
- data/docs/internals/bundles.md +4 -4
- data/docs/internals/engine.md +21 -8
- data/docs/internals/prompt-caching.md +4 -0
- data/docs/internals/tool-guardrails.md +10 -2
- data/docs/memory.md +6 -0
- data/docs/plugins.md +30 -13
- data/docs/releasing.md +3 -0
- data/docs/sessions.md +4 -4
- data/lib/samagotchi/bridge.rb +11 -7
- data/lib/samagotchi/bundle_command/check.rb +43 -12
- data/lib/samagotchi/bundle_command/scaffold.rb +12 -0
- data/lib/samagotchi/bundle_command/test_run.rb +12 -0
- data/lib/samagotchi/bundle_command/try/command.rb +164 -0
- data/lib/samagotchi/bundle_command/try/render.rb +80 -0
- data/lib/samagotchi/bundle_command/try/run.rb +35 -17
- data/lib/samagotchi/bundle_command/try/sandbox.rb +113 -34
- data/lib/samagotchi/bundle_command/try/trace.rb +26 -15
- data/lib/samagotchi/bundle_command.rb +11 -213
- data/lib/samagotchi/bundle_session_settings.rb +82 -45
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +6 -5
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +57 -12
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +5 -0
- data/lib/samagotchi/child_env.rb +50 -0
- data/lib/samagotchi/client_id.rb +3 -0
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_note.rb +13 -6
- data/lib/samagotchi/engine.rb +31 -15
- data/lib/samagotchi/hooks/registry.rb +2 -0
- data/lib/samagotchi/kernel_loop.rb +14 -3
- data/lib/samagotchi/last_turn.rb +3 -2
- data/lib/samagotchi/llm/chat_loop.rb +3 -1
- data/lib/samagotchi/llm/errors.rb +4 -4
- data/lib/samagotchi/memory_bundle/builder.rb +3 -2
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +7 -2
- data/lib/samagotchi/memory_bundle/installer.rb +1 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +42 -8
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
- data/lib/samagotchi/parent_report.rb +12 -0
- data/lib/samagotchi/reply_wait.rb +6 -2
- data/lib/samagotchi/sandbox_layout.rb +66 -0
- data/lib/samagotchi/send_command.rb +3 -3
- data/lib/samagotchi/session_manager.rb +12 -20
- data/lib/samagotchi/task_finish_note.rb +90 -0
- data/lib/samagotchi/task_watch.rb +228 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +19 -4
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -2
- data/lib/samagotchi/testing/bundle.rb +32 -19
- data/lib/samagotchi/testing/install.rb +26 -17
- data/lib/samagotchi/testing/sandbox.rb +45 -37
- data/lib/samagotchi/testing/waiting.rb +5 -7
- data/lib/samagotchi/tool_declarations.rb +16 -0
- data/lib/samagotchi/tool_response.rb +28 -6
- data/lib/samagotchi/tools/builtins.rb +5 -1
- data/lib/samagotchi/tools/task_record.rb +9 -0
- data/lib/samagotchi/tools/task_runtime.rb +2 -2
- data/lib/samagotchi/tools/task_wait.rb +10 -3
- data/lib/samagotchi/turn_checkpoint.rb +23 -10
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +24 -14
- data/lib/samagotchi/web/public/cards_place.js +4 -1
- data/lib/samagotchi/web/public/images.js +6 -0
- data/lib/samagotchi/web/public/model_pick.js +10 -0
- data/lib/samagotchi/web/public/notify.js +4 -4
- data/lib/samagotchi/web/public/toasts.js +17 -9
- data/lib/samagotchi/web/public/turn_events.js +13 -1
- data/lib/samagotchi/web/public/unqueue.js +20 -4
- data/lib/samagotchi/worker.rb +104 -42
- data/lib/samagotchi/worker_idle_exit.rb +26 -3
- data/lib/samagotchi/worker_inbound.rb +18 -4
- data/lib/samagotchi/worker_task_wakes.rb +160 -0
- data/lib/samagotchi/worker_wakes.rb +11 -4
- metadata +8 -1
|
@@ -11,6 +11,7 @@ require_relative "context_note"
|
|
|
11
11
|
require_relative "log"
|
|
12
12
|
require_relative "session"
|
|
13
13
|
require_relative "session_inbox"
|
|
14
|
+
require_relative "tools/task_record"
|
|
14
15
|
require_relative "worker_wakes"
|
|
15
16
|
|
|
16
17
|
module Samagotchi
|
|
@@ -51,6 +52,19 @@ module Samagotchi
|
|
|
51
52
|
# that agrees closes the round and counts toward session.max_wakes only.
|
|
52
53
|
def peer_turn? = peer? && !delivery.agreed
|
|
53
54
|
def origin = { client_id: "#{ClientId::CONTEXT_PREFIX}#{name}" }
|
|
55
|
+
# The plain note #unmark_wake puts back.
|
|
56
|
+
def note = delivery.note
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# A task finish wake turn to run (TaskWatch, TaskFinishNote): its wake
|
|
60
|
+
# note is in the conversation as the start of the turn +turn_id+;
|
|
61
|
+
# +plain_note+ is the same note without the turn start or the ask, which
|
|
62
|
+
# #unmark_wake puts back after a failed or restored wake turn. +id+: the
|
|
63
|
+
# finished task it runs for (the others finished with it are plain notes).
|
|
64
|
+
TaskWake = Data.define(:id, :turn_id, :plain_note) do
|
|
65
|
+
def name = "task #{Tools::TaskRecord.short_id(id)}"
|
|
66
|
+
def origin = { client_id: "#{ClientId::TASK_PREFIX}#{id}" }
|
|
67
|
+
def note = plain_note
|
|
54
68
|
end
|
|
55
69
|
|
|
56
70
|
# A save between turns (the turn's own, a command's, the notes', the
|
|
@@ -144,13 +158,13 @@ module Samagotchi
|
|
|
144
158
|
|
|
145
159
|
# A failed wake turn kept nothing but its note: the note goes back to
|
|
146
160
|
# a plain note (no turn start, the background wording), so a reload
|
|
147
|
-
# draws no empty turn for it.
|
|
148
|
-
def unmark_wake(
|
|
161
|
+
# draws no empty turn for it. +wake+: a ContextWake or a TaskWake.
|
|
162
|
+
def unmark_wake(wake)
|
|
149
163
|
messages = @engine.messages_checkpoint
|
|
150
|
-
index = messages.index { |m| m[:turn_start] && m[:turn_id] ==
|
|
164
|
+
index = messages.index { |m| m[:turn_start] && m[:turn_id] == wake.turn_id }
|
|
151
165
|
return unless index
|
|
152
166
|
|
|
153
|
-
messages[index] = ContextNote.message(**
|
|
167
|
+
messages[index] = ContextNote.message(**wake.note)
|
|
154
168
|
@engine.rollback_to(messages)
|
|
155
169
|
end
|
|
156
170
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
require_relative "child_reports"
|
|
6
|
+
require_relative "kernel_loop"
|
|
7
|
+
require_relative "log"
|
|
8
|
+
require_relative "task_finish_note"
|
|
9
|
+
require_relative "task_watch"
|
|
10
|
+
require_relative "tools/task_runtime"
|
|
11
|
+
require_relative "worker_inbound"
|
|
12
|
+
require_relative "worker_wakes"
|
|
13
|
+
|
|
14
|
+
module Samagotchi
|
|
15
|
+
# A worker's finish wake (docs/internals/background-tasks.md, Finish
|
|
16
|
+
# wake): its TaskWatch, fed by the Engine's raw events (#observe), and
|
|
17
|
+
# the TASK FINISHED notes for the tasks that finished unseen. #take
|
|
18
|
+
# writes them and hands back the wake turn to run, which the Worker runs
|
|
19
|
+
# (Worker#run_wake); #absorb gives them to a prompt's turn as plain notes.
|
|
20
|
+
# On the loop's thread, like WorkerInbound.
|
|
21
|
+
class WorkerTaskWakes
|
|
22
|
+
# @return [TaskWatch]
|
|
23
|
+
attr_reader :watch
|
|
24
|
+
|
|
25
|
+
# @param wakes [WorkerWakes] the wake budget a task's wake asks
|
|
26
|
+
# @param awaiting_continue [#call] whether a continue offer waits
|
|
27
|
+
# @param idle [#call] nothing queued and no exit asked for: a finish
|
|
28
|
+
# may start a turn
|
|
29
|
+
# @param save [#call] saves the session (a failure is the callee's to log)
|
|
30
|
+
def initialize(engine:, session:, wakes:, awaiting_continue:, idle:, save:)
|
|
31
|
+
@engine = engine
|
|
32
|
+
@session = session
|
|
33
|
+
@wakes = wakes
|
|
34
|
+
@awaiting_continue = awaiting_continue
|
|
35
|
+
@idle = idle
|
|
36
|
+
@save = save
|
|
37
|
+
@watch = TaskWatch.new
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The Engine's raw events feed the task watch (session.task_wakes not
|
|
41
|
+
# off): a result that started a task or showed its end, a turn's start,
|
|
42
|
+
# a user's Stop of it (M5).
|
|
43
|
+
def observe(event)
|
|
44
|
+
case event[:type]
|
|
45
|
+
when :tool_call_completed then @watch.observe(event) unless TaskWatch.mode == "off"
|
|
46
|
+
when :turn_started then @watch.turn_started
|
|
47
|
+
when :turn_canceled then @watch.turn_canceled(event[:cancellation_reason])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Whether a finished task would start a wake turn now: the Engine skips
|
|
52
|
+
# the turn-end warm-up then (next_turn_waiting).
|
|
53
|
+
def due?
|
|
54
|
+
TaskWatch.mode == "wake" && @watch.any? && @wakes.task_open?(awaiting_continue: @awaiting_continue.call) &&
|
|
55
|
+
@watch.finished?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The background tasks this session started that finished unseen
|
|
59
|
+
# (TaskWatch#take_finished), each as a TASK FINISHED note
|
|
60
|
+
# (TaskFinishNote): the first one not note-only starts a wake turn from
|
|
61
|
+
# task:<id> when session.task_wakes is wake, nothing is queued, no exit
|
|
62
|
+
# was asked for and the wake budget is open (WorkerWakes#task_open?);
|
|
63
|
+
# the others, and every one otherwise, are plain notes for the next
|
|
64
|
+
# turn (M2). A spent budget is announced once.
|
|
65
|
+
# @return [WorkerInbound::TaskWake, nil] the wake turn to run now
|
|
66
|
+
def take
|
|
67
|
+
return nil if @wakes.in_grace? || !@watch.any?
|
|
68
|
+
|
|
69
|
+
finished = take_finished
|
|
70
|
+
return nil if finished.empty?
|
|
71
|
+
|
|
72
|
+
waking = finished.find { |entry, _| !entry.note_only } if wake_open?
|
|
73
|
+
task_wake = note_finished(finished, waking: waking)
|
|
74
|
+
return nil if task_wake == :failed
|
|
75
|
+
|
|
76
|
+
@save.call
|
|
77
|
+
return task_wake if task_wake
|
|
78
|
+
|
|
79
|
+
budget_notice(finished.count { |entry, _| !entry.note_only }) if TaskWatch.mode == "wake" && @wakes.spent?
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Before a prompt's turn: the finished tasks join it as plain notes.
|
|
84
|
+
def absorb
|
|
85
|
+
return unless @watch.any?
|
|
86
|
+
|
|
87
|
+
finished = take_finished
|
|
88
|
+
return if finished.empty?
|
|
89
|
+
return if note_finished(finished) == :failed
|
|
90
|
+
|
|
91
|
+
@save.call
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# Each finished task's note into the conversation: +waking+'s as the
|
|
97
|
+
# wake turn's start, the others plain. A note that can't be written (a
|
|
98
|
+
# log that can't be read, say) puts it and the ones after it back in
|
|
99
|
+
# the watch for the next look.
|
|
100
|
+
# @return [WorkerInbound::TaskWake, nil, :failed]
|
|
101
|
+
def note_finished(finished, waking: nil)
|
|
102
|
+
written = []
|
|
103
|
+
(finished - [waking]).each do |entry, record|
|
|
104
|
+
add_note(entry, record)
|
|
105
|
+
written << entry
|
|
106
|
+
end
|
|
107
|
+
waking && add_wake_note(*waking)
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
Log.warn(:worker, "task_note_failed", error: e.class.name, msg: e.message)
|
|
110
|
+
@watch.put_back(finished.map(&:first) - written)
|
|
111
|
+
:failed
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Only the tasks the conversation still holds (a !rollback, a restored
|
|
115
|
+
# turn may have dropped the result that started one). A failure to
|
|
116
|
+
# read them is logged and counted (TaskWatch#look_failed: past
|
|
117
|
+
# TaskWatch::MAX_FAILURES they no longer hold the worker up): they wait
|
|
118
|
+
# for the next look.
|
|
119
|
+
def take_finished
|
|
120
|
+
@watch.take_finished(keep: Tools::TaskRuntime.created_ids_in(@engine.messages_checkpoint))
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
Log.warn(:worker, "task_watch_failed", error: e.class.name, msg: e.message)
|
|
123
|
+
@watch.look_failed
|
|
124
|
+
[]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def wake_open?
|
|
128
|
+
TaskWatch.mode == "wake" && @idle.call && @wakes.task_open?(awaiting_continue: @awaiting_continue.call)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def note(entry, record, wake:, turn_id: nil)
|
|
132
|
+
TaskFinishNote.note(record, limit_sec: entry.limit_sec, wake: wake, turn_id: turn_id,
|
|
133
|
+
cap: KernelLoop.resolve_output_char_cap(nil))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def add_note(entry, record)
|
|
137
|
+
@engine.add_context_note(@session, note(entry, record, wake: false))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The wake note, in the conversation as the wake turn's first message
|
|
141
|
+
# (before TurnFlow#before_prompt_turn, so a restore keeps it).
|
|
142
|
+
# @return [WorkerInbound::TaskWake]
|
|
143
|
+
def add_wake_note(entry, record)
|
|
144
|
+
turn_id = SecureRandom.uuid
|
|
145
|
+
@engine.add_context_note(@session, note(entry, record, wake: true, turn_id: turn_id))
|
|
146
|
+
WorkerInbound::TaskWake.new(id: entry.id, turn_id: turn_id, plain_note: note(entry, record, wake: false))
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The wake budget is spent: the UIs hear once that finished tasks wait
|
|
150
|
+
# as notes (one notice per stretch, shared with the other kinds).
|
|
151
|
+
def budget_notice(count)
|
|
152
|
+
return unless count.positive? && @wakes.notice_budget!
|
|
153
|
+
|
|
154
|
+
@engine.announce(type: :hook_notice, hook: "task", level: :info, between_turns: true,
|
|
155
|
+
text: "#{count} finished background task#{"s" if count != 1} noted; the model reads " \
|
|
156
|
+
"#{count == 1 ? "it" : "them"} with your next message " \
|
|
157
|
+
"(#{@wakes.max} wake turn#{"s" if @wakes.max != 1} in a row, #{ChildRing::MAX_WAKES_KEY})")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -6,10 +6,10 @@ require_relative "child_reports"
|
|
|
6
6
|
|
|
7
7
|
module Samagotchi
|
|
8
8
|
# A worker's wake budget: turns nobody typed (a delegate report's wake
|
|
9
|
-
# turn, an attached context source's) run
|
|
10
|
-
# most session.max_wakes in a row with no
|
|
11
|
-
# failed one until a human's input (a
|
|
12
|
-
# Shared by
|
|
9
|
+
# turn, a finished background task's, an attached context source's) run
|
|
10
|
+
# only past the start grace, at most session.max_wakes in a row with no
|
|
11
|
+
# human input, and not after a failed one until a human's input (a
|
|
12
|
+
# provider outage doesn't loop). Shared by every kind of wake; the Worker asks it before each and tells
|
|
13
13
|
# it what happened. It also holds the reports a delegate wake turn was
|
|
14
14
|
# started for until the turn's first boundary takes them (#hand_over,
|
|
15
15
|
# #take_handed).
|
|
@@ -110,6 +110,13 @@ module Samagotchi
|
|
|
110
110
|
true
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
# For a background task that finished unseen (TaskWatch): no continue
|
|
114
|
+
# offer, not paused, past the start grace and under the budget. #count!
|
|
115
|
+
# when its wake turn starts.
|
|
116
|
+
def task_open?(awaiting_continue:)
|
|
117
|
+
!@paused && !awaiting_continue && !in_grace? && @in_a_row < max
|
|
118
|
+
end
|
|
119
|
+
|
|
113
120
|
# A wake turn starts; +peer+: for a peer's answer, which counts toward
|
|
114
121
|
# both budgets. @return [Integer] the wake turns in a row now
|
|
115
122
|
def count!(peer: false)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.92.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -204,6 +204,8 @@ files:
|
|
|
204
204
|
- lib/samagotchi/bundle_command/check.rb
|
|
205
205
|
- lib/samagotchi/bundle_command/scaffold.rb
|
|
206
206
|
- lib/samagotchi/bundle_command/test_run.rb
|
|
207
|
+
- lib/samagotchi/bundle_command/try/command.rb
|
|
208
|
+
- lib/samagotchi/bundle_command/try/render.rb
|
|
207
209
|
- lib/samagotchi/bundle_command/try/run.rb
|
|
208
210
|
- lib/samagotchi/bundle_command/try/sandbox.rb
|
|
209
211
|
- lib/samagotchi/bundle_command/try/trace.rb
|
|
@@ -263,6 +265,7 @@ files:
|
|
|
263
265
|
- lib/samagotchi/bundles/system/self_map.md
|
|
264
266
|
- lib/samagotchi/cache_confirm.rb
|
|
265
267
|
- lib/samagotchi/cancellation_controller.rb
|
|
268
|
+
- lib/samagotchi/child_env.rb
|
|
266
269
|
- lib/samagotchi/child_move.rb
|
|
267
270
|
- lib/samagotchi/child_reports.rb
|
|
268
271
|
- lib/samagotchi/children_status.rb
|
|
@@ -492,6 +495,7 @@ files:
|
|
|
492
495
|
- lib/samagotchi/reply_wait.rb
|
|
493
496
|
- lib/samagotchi/row_refs.rb
|
|
494
497
|
- lib/samagotchi/sampling_settings.rb
|
|
498
|
+
- lib/samagotchi/sandbox_layout.rb
|
|
495
499
|
- lib/samagotchi/self_report.rb
|
|
496
500
|
- lib/samagotchi/send_command.rb
|
|
497
501
|
- lib/samagotchi/served_model.rb
|
|
@@ -520,6 +524,8 @@ files:
|
|
|
520
524
|
- lib/samagotchi/steer_cut.rb
|
|
521
525
|
- lib/samagotchi/steer_cut_after.rb
|
|
522
526
|
- lib/samagotchi/system_prompt.rb
|
|
527
|
+
- lib/samagotchi/task_finish_note.rb
|
|
528
|
+
- lib/samagotchi/task_watch.rb
|
|
523
529
|
- lib/samagotchi/terminal_ui.rb
|
|
524
530
|
- lib/samagotchi/terminal_ui/attach_launcher.rb
|
|
525
531
|
- lib/samagotchi/terminal_ui/attached_loop.rb
|
|
@@ -741,6 +747,7 @@ files:
|
|
|
741
747
|
- lib/samagotchi/worker_inbound.rb
|
|
742
748
|
- lib/samagotchi/worker_sidecar.rb
|
|
743
749
|
- lib/samagotchi/worker_signals.rb
|
|
750
|
+
- lib/samagotchi/worker_task_wakes.rb
|
|
744
751
|
- lib/samagotchi/worker_wakes.rb
|
|
745
752
|
- lib/samagotchi/yaml_lines.rb
|
|
746
753
|
homepage: https://github.com/dm1try/samagotchi
|