samagotchi 0.110.0 → 0.111.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -1
  3. data/docs/architecture.md +8 -5
  4. data/docs/cli.md +3 -0
  5. data/docs/configuration.md +43 -0
  6. data/docs/internals/engine.md +39 -10
  7. data/docs/internals/participants.md +1 -1
  8. data/docs/internals/remote-sessions.md +3 -3
  9. data/docs/internals/web-push.md +200 -0
  10. data/docs/internals/working-set.md +7 -6
  11. data/docs/sessions.md +1 -1
  12. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  13. data/lib/samagotchi/config.rb +4 -0
  14. data/lib/samagotchi/engine.rb +146 -490
  15. data/lib/samagotchi/engine_snapshots.rb +223 -0
  16. data/lib/samagotchi/event_fanout.rb +236 -0
  17. data/lib/samagotchi/model_binding.rb +138 -0
  18. data/lib/samagotchi/send_command/wait_target.rb +10 -3
  19. data/lib/samagotchi/send_command.rb +2 -1
  20. data/lib/samagotchi/session.rb +12 -3
  21. data/lib/samagotchi/session_access/remote.rb +2 -1
  22. data/lib/samagotchi/session_access.rb +7 -3
  23. data/lib/samagotchi/session_manager.rb +5 -0
  24. data/lib/samagotchi/turn_endings.rb +137 -0
  25. data/lib/samagotchi/version.rb +1 -1
  26. data/lib/samagotchi/web/app.rb +27 -1377
  27. data/lib/samagotchi/web/grants.rb +3 -12
  28. data/lib/samagotchi/web/message_display.rb +274 -0
  29. data/lib/samagotchi/web/private_files.rb +31 -0
  30. data/lib/samagotchi/web/public/app.js +30 -6
  31. data/lib/samagotchi/web/public/apple-touch-icon.png +0 -0
  32. data/lib/samagotchi/web/public/icons/chi-180.png +0 -0
  33. data/lib/samagotchi/web/public/icons/chi-192.png +0 -0
  34. data/lib/samagotchi/web/public/icons/chi-512.png +0 -0
  35. data/lib/samagotchi/web/public/index.html +4 -0
  36. data/lib/samagotchi/web/public/manifest.webmanifest +13 -0
  37. data/lib/samagotchi/web/public/notifications.js +137 -3
  38. data/lib/samagotchi/web/public/notify.js +1 -1
  39. data/lib/samagotchi/web/public/push.js +95 -0
  40. data/lib/samagotchi/web/public/stage_view.js +8 -0
  41. data/lib/samagotchi/web/public/sw.js +101 -0
  42. data/lib/samagotchi/web/public/turn_events.js +22 -0
  43. data/lib/samagotchi/web/push/attention.rb +105 -0
  44. data/lib/samagotchi/web/push/client.rb +103 -0
  45. data/lib/samagotchi/web/push/crypto.rb +126 -0
  46. data/lib/samagotchi/web/push/notifier.rb +230 -0
  47. data/lib/samagotchi/web/push/store.rb +285 -0
  48. data/lib/samagotchi/web/push.rb +73 -0
  49. data/lib/samagotchi/web/push_routes.rb +99 -0
  50. data/lib/samagotchi/web/server.rb +26 -2
  51. data/lib/samagotchi/web/session_control.rb +441 -0
  52. data/lib/samagotchi/web/session_create.rb +384 -0
  53. data/lib/samagotchi/web/session_show.rb +411 -0
  54. data/lib/samagotchi/worker.rb +3 -2
  55. metadata +25 -1
@@ -179,7 +179,8 @@ module Samagotchi
179
179
 
180
180
  json = reply.json
181
181
  Created.new(id: json.fetch("id"), model_warning: json["model_warning"], continues: json["continues"],
182
- moved_children: json["moved"] || [], parent_answers_warning: json["parent_answers_warning"])
182
+ moved_children: json["moved"] || [], parent_answers_warning: json["parent_answers_warning"],
183
+ first_turn_id: json["first_turn_id"])
183
184
  end
184
185
 
185
186
  # Whether a just-created session's worker came up within +timeout+
@@ -160,9 +160,13 @@ module Samagotchi
160
160
 
161
161
  # What chi send --new created (Remote; Local gives the Session): its
162
162
  # id, a model warning, the session it continues, the delegates it took
163
- # over, and the server's word on a --parent-answers looser than its
164
- # config.yml's (capped).
165
- Created = Data.define(:id, :model_warning, :continues, :moved_children, :parent_answers_warning)
163
+ # over, the server's word on a --parent-answers looser than its
164
+ # config.yml's (capped), and the id the prompt it started with gets for
165
+ # its first turn (nil for one created idle).
166
+ Created = Data.define(:id, :model_warning, :continues, :moved_children, :parent_answers_warning, :first_turn_id) do
167
+ def initialize(id:, model_warning: nil, continues: nil, moved_children: [], parent_answers_warning: nil,
168
+ first_turn_id: nil) = super
169
+ end
166
170
 
167
171
  # A session's worker as an attach finds it (Local#worker, chi web's GET
168
172
  # …/worker): +sidecar+ the live worker's WorkerSidecar (port, version,
@@ -197,6 +197,11 @@ module Samagotchi
197
197
  # the prompts), so the session starts idle.
198
198
  session.status = prompt.to_s.strip.empty? ? Session::STATUS_IDLE : Session::STATUS_RUNNING
199
199
  session.last_prompt = prompt
200
+ # The first prompt's turn gets an id like a sent message's, so a wait
201
+ # for it can name it (chi send --new --wait, chi web's create): the
202
+ # worker runs it with that origin, and its reply file and last_turn
203
+ # carry the id.
204
+ session.first_turn_id = SecureRandom.uuid unless prompt.to_s.strip.empty?
200
205
  # The worker takes last_prompt and clears it, and messages are saved at
201
206
  # the turn's end: until then this is the only preview a list has.
202
207
  session.first_preview = Session.preview_of(preview.to_s.strip.empty? ? prompt : preview)
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "llm/errors"
4
+ require_relative "tools/task_runtime"
5
+ require_relative "turn_note"
6
+
7
+ module Samagotchi
8
+ # What each of an Engine's turn endings keeps and announces: pure builders
9
+ # over the turn, its result or its error. The kept messages and their
10
+ # TurnNote (#kept_messages, #ctrl_c_messages, #failed_messages), the end
11
+ # event (#failed_event), the turn summary and the stop facts. The Engine
12
+ # forwards each here and keeps the steps that hold the event lock or save
13
+ # (end_turn, complete_turn, store_answer_display, record_last_turn).
14
+ class TurnEndings
15
+ # @param failure_summary [#call] (error) → a failed turn's one line (Engine#failure_summary)
16
+ def initialize(failure_summary:)
17
+ @failure_summary = failure_summary
18
+ end
19
+
20
+ # Why a turn stopped, from its end event (LastTurn::STOP_FACTS), for a
21
+ # parent agent's wait (ReplyWait): a provider error's kind and whether
22
+ # a retry may help, a cancel's reason and the hook that stopped it.
23
+ # Only what is known.
24
+ def stop_facts(event)
25
+ case event&.dig(:type)
26
+ # kept_steps: the turn's work stayed (a parent must not send the task again).
27
+ when :turn_failed
28
+ { error_kind: event[:error_kind]&.to_s, retryable: event[:retryable], kept_steps: event[:kept_steps] }
29
+ when :turn_canceled
30
+ { cancel_reason: event[:cancellation_reason]&.to_s, stopped_by: event[:cancelled_by]&.to_s }
31
+ else {}
32
+ end
33
+ end
34
+
35
+ # What the session keeps of a turn that ended normally (nil: nothing to
36
+ # replace). Runs under the event lock: it also adds the turn's note to
37
+ # result.conversation, which the REPL keeps as-is.
38
+ # A turn that ran out of iterations ends at its tool results, so a
39
+ # continue resumes from them rather than after a made-up reply (and a
40
+ # step-limit Continue from its run's start: ModelResult#run_start).
41
+ def kept_messages(turn, result, seconds)
42
+ conversation = result.conversation if result.conversation.is_a?(Array)
43
+ # Nothing visible (no text) is a turn the model should know ended
44
+ # that way: the note, and nothing made up as its answer. The note's
45
+ # marker is for the UIs (the notice, and the empty steps after a
46
+ # reload); it goes on the result's conversation too (the REPL keeps it).
47
+ # A retry's nudge at the tail goes: this note says it all. An earlier
48
+ # turn's note stays: it says why that turn ended.
49
+ if result.empty_answer?
50
+ note = TurnNote.empty(retries: result.empty_retries, steps: result.empty_steps, refusal: result.refusal)
51
+ conversation&.replace(TurnNote.without_trailing_nudge(conversation))
52
+ saved = TurnNote.without_trailing_nudge(conversation || turn.session.messages)
53
+ conversation << note if conversation
54
+ saved + [note]
55
+ elsif result.canceled? && conversation
56
+ conversation << TurnNote.cancelled(result.cancellation_reason, seconds: seconds,
57
+ stopped_by: turn.controller.detail,
58
+ shown: TurnNote.interrupted_tail?(conversation),
59
+ running_tasks: Tools::TaskRuntime.running_created_in(conversation))
60
+ else
61
+ conversation
62
+ end
63
+ end
64
+
65
+ # A Ctrl-C keeps the loop's conversation so far, as a Stop does (its
66
+ # finished tool iterations, every call answered: ToolResponse.run_batch),
67
+ # else the turn's messages so far (it came before the loop), with a
68
+ # cancel note naming the tasks still running; nil before there are any.
69
+ def ctrl_c_messages(turn, interrupt, seconds)
70
+ kept = loop_conversation(interrupt) || turn.messages
71
+ return nil unless kept
72
+
73
+ note = TurnNote.cancelled(:ctrl_c, seconds: seconds, running_tasks: Tools::TaskRuntime.running_created_in(kept))
74
+ TurnNote.replace_trailing(kept, note)
75
+ end
76
+
77
+ # The conversation the loop had built when +error+ ended the turn
78
+ # (LLM::FailedTurn), or nil.
79
+ def loop_conversation(error)
80
+ error.partial_conversation if error.respond_to?(:partial_conversation) && error.partial_conversation.is_a?(Array)
81
+ end
82
+
83
+ # A failure keeps the loop's partial conversation (else the turn's
84
+ # messages so far) with a failed note; nil when the turn never reached
85
+ # the model. The model reads why on its next turn. A turn that got to
86
+ # tool results (LLM::FailedTurn.progress) says so in the note; on the
87
+ # error (kept_steps) too unless it failed in a way that rolls back
88
+ # (FailedTurn.keeps_work?: a context overflow). The UIs keep a marked
89
+ # turn (TurnFlow#prompt_turn_failed), and roll back the others, with a
90
+ # note of their own; -p --non-interactive keeps what this keeps.
91
+ def failed_messages(turn, error)
92
+ kept = loop_conversation(error) || turn.messages
93
+ return nil unless kept
94
+
95
+ steps = LLM::FailedTurn.progress(turn.session.messages, kept)
96
+ error.kept_steps = steps if steps && error.is_a?(LLM::FailedTurn) && LLM::FailedTurn.keeps_work?(error)
97
+ summary = error.respond_to?(:summary) ? error.summary : error.message
98
+ TurnNote.replace_trailing(kept, TurnNote.failed(summary, continued: turn.continue, steps: steps))
99
+ end
100
+
101
+ # Called after #failed_messages, which marks the error's kept steps.
102
+ def failed_event(error, seconds)
103
+ failed = { type: :turn_failed, error_class: error.class.name, message: error.message,
104
+ duration_ms: (seconds * 1000).round }
105
+ # Its work stays: the UIs say so, as after a cancel.
106
+ steps = LLM::FailedTurn.kept_steps(error)
107
+ failed[:kept_steps] = steps if steps
108
+ # A provider error says what kind it is, for one line per kind in the UIs.
109
+ if error.is_a?(LLM::ProviderError)
110
+ failed.merge!(error_kind: error.kind, retryable: error.retryable?, host: error.host, summary: failure_summary(error))
111
+ end
112
+ failed
113
+ end
114
+
115
+ # JSON-safe digest of a finished turn for renderers (in-process or over the
116
+ # Bridge): the bits of the native loop's result a UI needs beyond `result:`.
117
+ # @param result [LLM::ModelResult]
118
+ # @return [Hash]
119
+ def turn_summary(result)
120
+ summary = {
121
+ output: result.output.to_s,
122
+ exhausted: result.exhausted?,
123
+ resumable: result.resumable?,
124
+ pending_tool_calls: result.pending_tool_calls?,
125
+ tool_activity: Array(result.tool_activity).map(&:dup),
126
+ context_status: result.context_status&.dup
127
+ }
128
+ # No answer: every UI shows its notice (TurnNote.empty_answer_line).
129
+ summary[:empty_answer] = { retries: result.empty_retries.to_i, refusal: result.refusal }.compact if result.empty_answer?
130
+ summary
131
+ end
132
+
133
+ private
134
+
135
+ def failure_summary(error) = @failure_summary.call(error)
136
+ end
137
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.110.0"
4
+ VERSION = "0.111.0"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end