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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -1
  3. data/README.md +5 -2
  4. data/bin/chi +4 -4
  5. data/docs/architecture.md +17 -13
  6. data/docs/bundle-authoring.md +6 -11
  7. data/docs/cli.md +7 -4
  8. data/docs/configuration.md +8 -4
  9. data/docs/context.md +5 -3
  10. data/docs/hooks.md +3 -3
  11. data/docs/internals/background-tasks.md +27 -2
  12. data/docs/internals/bundles.md +4 -4
  13. data/docs/internals/engine.md +21 -8
  14. data/docs/internals/prompt-caching.md +4 -0
  15. data/docs/internals/tool-guardrails.md +10 -2
  16. data/docs/memory.md +6 -0
  17. data/docs/plugins.md +30 -13
  18. data/docs/releasing.md +3 -0
  19. data/docs/sessions.md +4 -4
  20. data/lib/samagotchi/bridge.rb +11 -7
  21. data/lib/samagotchi/bundle_command/check.rb +43 -12
  22. data/lib/samagotchi/bundle_command/scaffold.rb +12 -0
  23. data/lib/samagotchi/bundle_command/test_run.rb +12 -0
  24. data/lib/samagotchi/bundle_command/try/command.rb +164 -0
  25. data/lib/samagotchi/bundle_command/try/render.rb +80 -0
  26. data/lib/samagotchi/bundle_command/try/run.rb +35 -17
  27. data/lib/samagotchi/bundle_command/try/sandbox.rb +113 -34
  28. data/lib/samagotchi/bundle_command/try/trace.rb +26 -15
  29. data/lib/samagotchi/bundle_command.rb +11 -213
  30. data/lib/samagotchi/bundle_session_settings.rb +82 -45
  31. data/lib/samagotchi/bundles/core/manifest.yml +2 -2
  32. data/lib/samagotchi/bundles/cost-cap/manifest.yml +6 -5
  33. data/lib/samagotchi/bundles/cost-cap/plugin.rb +57 -12
  34. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  35. data/lib/samagotchi/bundles/system/self_map.md +5 -0
  36. data/lib/samagotchi/child_env.rb +50 -0
  37. data/lib/samagotchi/client_id.rb +3 -0
  38. data/lib/samagotchi/config.rb +3 -0
  39. data/lib/samagotchi/context_note.rb +13 -6
  40. data/lib/samagotchi/engine.rb +31 -15
  41. data/lib/samagotchi/hooks/registry.rb +2 -0
  42. data/lib/samagotchi/kernel_loop.rb +14 -3
  43. data/lib/samagotchi/last_turn.rb +3 -2
  44. data/lib/samagotchi/llm/chat_loop.rb +3 -1
  45. data/lib/samagotchi/llm/errors.rb +4 -4
  46. data/lib/samagotchi/memory_bundle/builder.rb +3 -2
  47. data/lib/samagotchi/memory_bundle/installed_bundle.rb +7 -2
  48. data/lib/samagotchi/memory_bundle/installer.rb +1 -0
  49. data/lib/samagotchi/memory_bundle/manifest.rb +42 -8
  50. data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
  51. data/lib/samagotchi/parent_report.rb +12 -0
  52. data/lib/samagotchi/reply_wait.rb +6 -2
  53. data/lib/samagotchi/sandbox_layout.rb +66 -0
  54. data/lib/samagotchi/send_command.rb +3 -3
  55. data/lib/samagotchi/session_manager.rb +12 -20
  56. data/lib/samagotchi/task_finish_note.rb +90 -0
  57. data/lib/samagotchi/task_watch.rb +228 -0
  58. data/lib/samagotchi/terminal_ui/attached_loop.rb +19 -4
  59. data/lib/samagotchi/terminal_ui/formatting.rb +7 -2
  60. data/lib/samagotchi/testing/bundle.rb +32 -19
  61. data/lib/samagotchi/testing/install.rb +26 -17
  62. data/lib/samagotchi/testing/sandbox.rb +45 -37
  63. data/lib/samagotchi/testing/waiting.rb +5 -7
  64. data/lib/samagotchi/tool_declarations.rb +16 -0
  65. data/lib/samagotchi/tool_response.rb +28 -6
  66. data/lib/samagotchi/tools/builtins.rb +5 -1
  67. data/lib/samagotchi/tools/task_record.rb +9 -0
  68. data/lib/samagotchi/tools/task_runtime.rb +2 -2
  69. data/lib/samagotchi/tools/task_wait.rb +10 -3
  70. data/lib/samagotchi/turn_checkpoint.rb +23 -10
  71. data/lib/samagotchi/version.rb +1 -1
  72. data/lib/samagotchi/web/public/app.js +24 -14
  73. data/lib/samagotchi/web/public/cards_place.js +4 -1
  74. data/lib/samagotchi/web/public/images.js +6 -0
  75. data/lib/samagotchi/web/public/model_pick.js +10 -0
  76. data/lib/samagotchi/web/public/notify.js +4 -4
  77. data/lib/samagotchi/web/public/toasts.js +17 -9
  78. data/lib/samagotchi/web/public/turn_events.js +13 -1
  79. data/lib/samagotchi/web/public/unqueue.js +20 -4
  80. data/lib/samagotchi/worker.rb +104 -42
  81. data/lib/samagotchi/worker_idle_exit.rb +26 -3
  82. data/lib/samagotchi/worker_inbound.rb +18 -4
  83. data/lib/samagotchi/worker_task_wakes.rb +160 -0
  84. data/lib/samagotchi/worker_wakes.rb +11 -4
  85. 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(context_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] == context_wake.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(**context_wake.delivery.note)
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 only past the start grace, at
10
- # most session.max_wakes in a row with no human input, and not after a
11
- # failed one until a human's input (a provider outage doesn't loop).
12
- # Shared by both kinds of wake; the Worker asks it before each and tells
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.90.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