samagotchi 0.31.0 → 0.32.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 +42 -1
- data/README.md +13 -0
- data/bin/chi +3 -0
- data/docs/cli.md +1 -0
- data/docs/configuration.md +3 -1
- data/docs/context.md +218 -0
- data/docs/guardrails.md +12 -4
- data/docs/hooks.md +1 -0
- data/docs/plugins.md +77 -1
- data/docs/releasing.md +1 -1
- data/lib/samagotchi/bootstrap_command.rb +4 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +22 -0
- data/lib/samagotchi/bundles/github-pr/plugin.rb +65 -0
- data/lib/samagotchi/bundles/github-pr/scripts/pr_context.rb +199 -0
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +15 -2
- data/lib/samagotchi/bundles/guardrails/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +6 -0
- data/lib/samagotchi/child_reports.rb +0 -2
- data/lib/samagotchi/config.rb +6 -0
- data/lib/samagotchi/context_absorber.rb +179 -0
- data/lib/samagotchi/context_command.rb +473 -0
- data/lib/samagotchi/context_fetch.rb +226 -0
- data/lib/samagotchi/context_note.rb +11 -5
- data/lib/samagotchi/context_poller.rb +100 -0
- data/lib/samagotchi/context_providers.rb +159 -0
- data/lib/samagotchi/context_sources.rb +452 -0
- data/lib/samagotchi/engine.rb +10 -5
- data/lib/samagotchi/guardrail_wiring.rb +3 -1
- data/lib/samagotchi/guardrails/parent_approvals.rb +9 -7
- data/lib/samagotchi/guardrails/protected_paths.rb +8 -5
- data/lib/samagotchi/guardrails/shell_lex.rb +0 -9
- data/lib/samagotchi/guardrails/verdict.rb +21 -3
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/memory_bundle/installer.rb +43 -1
- data/lib/samagotchi/memory_bundle/manifest.rb +39 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +17 -1
- data/lib/samagotchi/plugin/attached_context.rb +100 -0
- data/lib/samagotchi/plugin/context.rb +22 -0
- data/lib/samagotchi/question_desk.rb +0 -3
- data/lib/samagotchi/session.rb +7 -5
- data/lib/samagotchi/session_commands.rb +18 -1
- data/lib/samagotchi/session_manager.rb +5 -2
- data/lib/samagotchi/session_retention.rb +3 -0
- data/lib/samagotchi/steer.rb +12 -3
- data/lib/samagotchi/system_prompt.rb +1 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +8 -4
- data/lib/samagotchi/terminal_ui/formatting.rb +14 -3
- data/lib/samagotchi/terminal_ui.rb +0 -3
- data/lib/samagotchi/tool_activity.rb +8 -0
- data/lib/samagotchi/tool_declarations.rb +21 -0
- data/lib/samagotchi/tools/builtin_calls.rb +1 -2
- data/lib/samagotchi/tools/builtins.rb +5 -0
- data/lib/samagotchi/tools/context_read.rb +163 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +109 -1
- data/lib/samagotchi/web/public/app.js +40 -10
- data/lib/samagotchi/web/public/composer_size.js +7 -6
- data/lib/samagotchi/web/public/context_chips.js +285 -0
- data/lib/samagotchi/web/public/data.js +21 -0
- data/lib/samagotchi/web/public/index.html +49 -3
- data/lib/samagotchi/web/public/notify.js +5 -3
- data/lib/samagotchi/web/public/timing.js +7 -3
- data/lib/samagotchi/web/public/turn_events.js +8 -2
- data/lib/samagotchi/web/public/turn_view.js +4 -1
- data/lib/samagotchi/worker.rb +126 -0
- metadata +14 -1
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "time"
|
|
4
6
|
|
|
7
|
+
require_relative "config"
|
|
5
8
|
require_relative "events"
|
|
6
9
|
require_relative "session"
|
|
7
10
|
require_relative "session_inbox"
|
|
@@ -18,6 +21,8 @@ require_relative "iteration_limit"
|
|
|
18
21
|
require_relative "session_commands"
|
|
19
22
|
require_relative "model_profile"
|
|
20
23
|
require_relative "child_reports"
|
|
24
|
+
require_relative "context_absorber"
|
|
25
|
+
require_relative "context_poller"
|
|
21
26
|
require_relative "tools/task_runtime"
|
|
22
27
|
|
|
23
28
|
module Samagotchi
|
|
@@ -51,6 +56,9 @@ module Samagotchi
|
|
|
51
56
|
# gets it over the Bridge a moment after it starts, and that message's
|
|
52
57
|
# turn goes first (the reports join it).
|
|
53
58
|
WAKE_START_GRACE = 2.0
|
|
59
|
+
# One context wake per source in this long (D5): later changes inside
|
|
60
|
+
# it arrive as notes.
|
|
61
|
+
CONTEXT_WAKE_WINDOW = 600
|
|
54
62
|
# How much of a command's output goes into its :command_ran.
|
|
55
63
|
COMMAND_OUTPUT_LIMIT = 4096
|
|
56
64
|
BUSY_OUTPUT = "busy: wait for the turn to end"
|
|
@@ -168,6 +176,9 @@ module Samagotchi
|
|
|
168
176
|
})
|
|
169
177
|
# This session's own delegate children's news (rings in children/).
|
|
170
178
|
@child_reports = ChildReports.new(session_id: @session_id, state_dir: @state_dir)
|
|
179
|
+
# Its attached context's notes (ContextSources).
|
|
180
|
+
@context_absorber = ContextAbsorber.new(session_id: @session_id, state_dir: @state_dir,
|
|
181
|
+
project_root: @session.project_root)
|
|
171
182
|
# /model's default is the config's, as in the REPL; the Engine started
|
|
172
183
|
# on the session's model.
|
|
173
184
|
@default_model = ModelProfile.required_model_name(nil)
|
|
@@ -211,6 +222,11 @@ module Samagotchi
|
|
|
211
222
|
# the background, shown by the UIs; a turn waits only for the ones
|
|
212
223
|
# that bring tools.
|
|
213
224
|
@engine.start_init_tasks!
|
|
225
|
+
# Attached context's commands, in the background; a new text wakes
|
|
226
|
+
# the loop to absorb it. Not activity: the idle exit stops it.
|
|
227
|
+
@context_poller = ContextPoller.new(session_id: @session_id, state_dir: @state_dir,
|
|
228
|
+
project_root: @session.project_root, cwd: @session.working_directory,
|
|
229
|
+
on_change: -> { @waker.wake }).start
|
|
214
230
|
loop do
|
|
215
231
|
# Check if the session was externally marked as stopped. Not
|
|
216
232
|
# stopped_on_disk?, which reads a vanished file as "not stopped":
|
|
@@ -223,7 +239,10 @@ module Samagotchi
|
|
|
223
239
|
next if run_queued_commands
|
|
224
240
|
|
|
225
241
|
# Between turns, so the next turn (the first one too) sees them.
|
|
242
|
+
# A change that asks to wake may start a turn here (C4), but not
|
|
243
|
+
# ahead of an exit or restart a client asked for: it is a note then.
|
|
226
244
|
absorb_notes
|
|
245
|
+
next if absorb_context(wake: nothing_queued? && !@exit_requested)
|
|
227
246
|
|
|
228
247
|
if (prompt = take_initial_prompt)
|
|
229
248
|
run_initial_prompt(prompt) unless initial_command(prompt)
|
|
@@ -263,6 +282,9 @@ module Samagotchi
|
|
|
263
282
|
# step-limit question stays in the file (a save here would write
|
|
264
283
|
# over a stop's status): with no live worker the lists don't read
|
|
265
284
|
# it as waiting, and the next worker drops it (drop_dead_question).
|
|
285
|
+
# The context poller first: a command it runs goes (its process
|
|
286
|
+
# group) before the Engine does.
|
|
287
|
+
@context_poller&.stop
|
|
266
288
|
@engine&.shutdown
|
|
267
289
|
@bridge&.stop
|
|
268
290
|
end
|
|
@@ -372,6 +394,108 @@ module Samagotchi
|
|
|
372
394
|
claimed.each { |file| FileUtils.rm_f(file) }
|
|
373
395
|
end
|
|
374
396
|
|
|
397
|
+
# The attached context's notes (ContextAbsorber), between turns. Not
|
|
398
|
+
# into a session with no turn yet (+before_turn+: one is about to
|
|
399
|
+
# run): auto-attached context alone doesn't make a session worth
|
|
400
|
+
# keeping. Saved, then the subscriptions written (a crash between the
|
|
401
|
+
# two re-delivers; the note ids dedupe).
|
|
402
|
+
# +wake+: the session is idle with nothing queued, so an update whose
|
|
403
|
+
# source asked to wake may start a turn (#context_wake_for): its note
|
|
404
|
+
# goes in as the turn's first message.
|
|
405
|
+
# @return [Boolean] whether a wake turn ran
|
|
406
|
+
def absorb_context(before_turn: false, wake: false)
|
|
407
|
+
return false unless before_turn || conversation_started?
|
|
408
|
+
return false if stopped_on_disk?
|
|
409
|
+
|
|
410
|
+
batch = @context_absorber.pending
|
|
411
|
+
return false unless batch
|
|
412
|
+
|
|
413
|
+
waking = wake ? context_wake_for(batch) : nil
|
|
414
|
+
turn_id = SecureRandom.uuid if waking
|
|
415
|
+
batch.deliveries.each do |delivery|
|
|
416
|
+
next unless delivery.note
|
|
417
|
+
|
|
418
|
+
note = delivery == waking ? delivery.wake_note.merge(turn_start: true, turn_id: turn_id) : delivery.note
|
|
419
|
+
@engine.add_context_note(@session, note)
|
|
420
|
+
end
|
|
421
|
+
# Saved even when every note was there already: after a failed save
|
|
422
|
+
# they are in memory only.
|
|
423
|
+
return false if batch.notes.any? && !save_or_log(:context) { @session.save(state_dir: @state_dir) }
|
|
424
|
+
|
|
425
|
+
@context_absorber.commit(waking ? woken(batch, waking) : batch)
|
|
426
|
+
return false unless waking
|
|
427
|
+
|
|
428
|
+
run_context_wake_turn(waking.name, turn_id)
|
|
429
|
+
true
|
|
430
|
+
rescue SystemCallError, IOError => e
|
|
431
|
+
Log.exception(:worker, "context_failed", e)
|
|
432
|
+
false
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# No input file or queued command waits: the loop would go idle.
|
|
436
|
+
def nothing_queued? = @command_queue.empty? && SessionInbox.find_new_input_files(@session_dir).empty?
|
|
437
|
+
|
|
438
|
+
# The update in +batch+ that may start a wake turn now: its source
|
|
439
|
+
# asked (wake: true), context.wake is on, the shared wake step's
|
|
440
|
+
# conditions hold (no continue offer, wakes not paused by a failed one,
|
|
441
|
+
# under session.max_wakes in a row with no human input, past the start
|
|
442
|
+
# grace) and the source hasn't woken the session in the last
|
|
443
|
+
# CONTEXT_WAKE_WINDOW. The others stay plain notes. Within the start
|
|
444
|
+
# grace a change is a note: it came while the session was away.
|
|
445
|
+
def context_wake_for(batch, now: Time.now)
|
|
446
|
+
candidates = batch.deliveries.select(&:wake_note)
|
|
447
|
+
return nil if candidates.empty? || !context_wakes_on?
|
|
448
|
+
return nil if @wakes_paused || @turn_flow.awaiting_continue? || wake_grace_left.positive?
|
|
449
|
+
|
|
450
|
+
if @wakes_in_a_row >= max_wakes
|
|
451
|
+
Log.info(:worker, "context_wake_held", reason: "max_wakes", names: candidates.map(&:name).join(","))
|
|
452
|
+
return nil
|
|
453
|
+
end
|
|
454
|
+
candidates.find { |delivery| !woke_lately?(delivery.subscription, now) }
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def context_wakes_on? = Config.get("context.wake") != false
|
|
458
|
+
|
|
459
|
+
def woke_lately?(subscription, now)
|
|
460
|
+
at = subscription&.wakes_at && Time.iso8601(subscription.wakes_at)
|
|
461
|
+
at ? now - at < CONTEXT_WAKE_WINDOW : false
|
|
462
|
+
rescue ArgumentError
|
|
463
|
+
false
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# +batch+ with +waking+'s subscription recording the wake (the 10-minute window).
|
|
467
|
+
def woken(batch, waking)
|
|
468
|
+
stamped = waking.with(subscription: waking.subscription.with(wakes_at: Time.now.iso8601))
|
|
469
|
+
batch.with(deliveries: batch.deliveries.map { |delivery| delivery == waking ? stamped : delivery })
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# A turn nobody typed for a changed source (a context wake): a continue
|
|
473
|
+
# turn from context:<name> whose first message is the wake note already
|
|
474
|
+
# in the conversation (named by +turn_id+), on the same rules as a
|
|
475
|
+
# delegate report's wake turn: it counts toward session.max_wakes, and
|
|
476
|
+
# a failure goes back to before the turn (the note stays: it was saved
|
|
477
|
+
# before the checkpoint) and pauses wakes until a human's input.
|
|
478
|
+
def run_context_wake_turn(name, turn_id)
|
|
479
|
+
@wakes_in_a_row += 1
|
|
480
|
+
Log.info(:worker, "context_wake_turn", name: name, in_a_row: @wakes_in_a_row)
|
|
481
|
+
@turn_flow.before_prompt_turn
|
|
482
|
+
run_engine_turn(nil, continue: true, origin: { client_id: "#{Steer::CONTEXT_CLIENT_PREFIX}#{name}" },
|
|
483
|
+
id: turn_id, max_iterations: IterationLimit.for) do |result, error|
|
|
484
|
+
if error
|
|
485
|
+
note = TurnNote.failed(error.respond_to?(:summary) ? error.summary : error.message)
|
|
486
|
+
@turn_flow.prompt_turn_failed(note: note)
|
|
487
|
+
@wakes_paused = true
|
|
488
|
+
else
|
|
489
|
+
@continue_offer.after_turn(result)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Whether the session has had a turn: a user or assistant message.
|
|
495
|
+
def conversation_started?
|
|
496
|
+
Array(@session.messages).any? { |m| %w[user assistant model].include?((m[:role] || m["role"]).to_s) }
|
|
497
|
+
end
|
|
498
|
+
|
|
375
499
|
# The first prompt (spawn_session's): a delegate child's task, which is
|
|
376
500
|
# its parent's although no client sent it.
|
|
377
501
|
def run_initial_prompt(prompt)
|
|
@@ -400,6 +524,8 @@ module Samagotchi
|
|
|
400
524
|
# its continue turn
|
|
401
525
|
# @param images [Array<Hash>] the prompt's image refs ({file:, name:})
|
|
402
526
|
def run_prompt(prompt, origin, no_interrupt: false, images: [])
|
|
527
|
+
# A first turn sees the context attached before it.
|
|
528
|
+
absorb_context(before_turn: true)
|
|
403
529
|
user_input(origin&.dig(:client_id))
|
|
404
530
|
@continue_offer.drop(origin)
|
|
405
531
|
@turn_flow.before_prompt_turn
|
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.32.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -125,6 +125,7 @@ files:
|
|
|
125
125
|
- docs/architecture.md
|
|
126
126
|
- docs/cli.md
|
|
127
127
|
- docs/configuration.md
|
|
128
|
+
- docs/context.md
|
|
128
129
|
- docs/desktop.md
|
|
129
130
|
- docs/guardrails.md
|
|
130
131
|
- docs/hooks.md
|
|
@@ -170,6 +171,9 @@ files:
|
|
|
170
171
|
- lib/samagotchi/bundles/check-in/plugin.rb
|
|
171
172
|
- lib/samagotchi/bundles/core/manifest.yml
|
|
172
173
|
- lib/samagotchi/bundles/dev/manifest.yml
|
|
174
|
+
- lib/samagotchi/bundles/github-pr/manifest.yml
|
|
175
|
+
- lib/samagotchi/bundles/github-pr/plugin.rb
|
|
176
|
+
- lib/samagotchi/bundles/github-pr/scripts/pr_context.rb
|
|
173
177
|
- lib/samagotchi/bundles/guardrails/guardrails.md
|
|
174
178
|
- lib/samagotchi/bundles/guardrails/guardrails/rules.yml
|
|
175
179
|
- lib/samagotchi/bundles/guardrails/guardrails/small-models.yml
|
|
@@ -203,8 +207,14 @@ files:
|
|
|
203
207
|
- lib/samagotchi/commands/registry.rb
|
|
204
208
|
- lib/samagotchi/config.rb
|
|
205
209
|
- lib/samagotchi/config_text_edit.rb
|
|
210
|
+
- lib/samagotchi/context_absorber.rb
|
|
211
|
+
- lib/samagotchi/context_command.rb
|
|
212
|
+
- lib/samagotchi/context_fetch.rb
|
|
206
213
|
- lib/samagotchi/context_note.rb
|
|
214
|
+
- lib/samagotchi/context_poller.rb
|
|
215
|
+
- lib/samagotchi/context_providers.rb
|
|
207
216
|
- lib/samagotchi/context_quote.rb
|
|
217
|
+
- lib/samagotchi/context_sources.rb
|
|
208
218
|
- lib/samagotchi/context_status.rb
|
|
209
219
|
- lib/samagotchi/context_usage.rb
|
|
210
220
|
- lib/samagotchi/context_window.rb
|
|
@@ -315,6 +325,7 @@ files:
|
|
|
315
325
|
- lib/samagotchi/paths.rb
|
|
316
326
|
- lib/samagotchi/pending_input_queue.rb
|
|
317
327
|
- lib/samagotchi/plugin/api.rb
|
|
328
|
+
- lib/samagotchi/plugin/attached_context.rb
|
|
318
329
|
- lib/samagotchi/plugin/context.rb
|
|
319
330
|
- lib/samagotchi/plugin/loader.rb
|
|
320
331
|
- lib/samagotchi/plugin/service.rb
|
|
@@ -394,6 +405,7 @@ files:
|
|
|
394
405
|
- lib/samagotchi/tools/builtin_calls.rb
|
|
395
406
|
- lib/samagotchi/tools/builtins.rb
|
|
396
407
|
- lib/samagotchi/tools/cancel_reminder.rb
|
|
408
|
+
- lib/samagotchi/tools/context_read.rb
|
|
397
409
|
- lib/samagotchi/tools/delegate.rb
|
|
398
410
|
- lib/samagotchi/tools/delegate_cursor.rb
|
|
399
411
|
- lib/samagotchi/tools/delegate_relay.rb
|
|
@@ -448,6 +460,7 @@ files:
|
|
|
448
460
|
- lib/samagotchi/web/public/command_list.js
|
|
449
461
|
- lib/samagotchi/web/public/command_view.js
|
|
450
462
|
- lib/samagotchi/web/public/composer_size.js
|
|
463
|
+
- lib/samagotchi/web/public/context_chips.js
|
|
451
464
|
- lib/samagotchi/web/public/copy.js
|
|
452
465
|
- lib/samagotchi/web/public/ctx.js
|
|
453
466
|
- lib/samagotchi/web/public/data.js
|