samagotchi 0.109.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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/bin/chi +5 -1
  4. data/docs/architecture.md +9 -6
  5. data/docs/cli.md +43 -1
  6. data/docs/configuration.md +140 -3
  7. data/docs/internals/engine.md +58 -14
  8. data/docs/internals/participants.md +7 -4
  9. data/docs/internals/prompt-caching.md +5 -0
  10. data/docs/internals/remote-sessions.md +3 -3
  11. data/docs/internals/web-push.md +200 -0
  12. data/docs/internals/working-set.md +7 -6
  13. data/docs/plugins.md +1 -1
  14. data/docs/sessions.md +4 -2
  15. data/docs/sub-agent.md +1 -1
  16. data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
  17. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  18. data/lib/samagotchi/config.rb +4 -0
  19. data/lib/samagotchi/context_note.rb +7 -3
  20. data/lib/samagotchi/engine.rb +166 -499
  21. data/lib/samagotchi/engine_snapshots.rb +223 -0
  22. data/lib/samagotchi/event_fanout.rb +236 -0
  23. data/lib/samagotchi/idle_recap.rb +1 -1
  24. data/lib/samagotchi/idle_reminders.rb +1 -1
  25. data/lib/samagotchi/kernel_loop.rb +12 -7
  26. data/lib/samagotchi/llm/chat_loop.rb +9 -2
  27. data/lib/samagotchi/message_time.rb +104 -0
  28. data/lib/samagotchi/model_binding.rb +138 -0
  29. data/lib/samagotchi/reminder_queue.rb +20 -3
  30. data/lib/samagotchi/reply_wait/poll.rb +39 -1
  31. data/lib/samagotchi/run_options.rb +4 -1
  32. data/lib/samagotchi/send_command/wait_target.rb +10 -3
  33. data/lib/samagotchi/send_command.rb +2 -1
  34. data/lib/samagotchi/session.rb +12 -3
  35. data/lib/samagotchi/session_access/contract.rb +1 -1
  36. data/lib/samagotchi/session_access/local.rb +11 -5
  37. data/lib/samagotchi/session_access/remote.rb +2 -1
  38. data/lib/samagotchi/session_access.rb +7 -3
  39. data/lib/samagotchi/session_inbox.rb +7 -3
  40. data/lib/samagotchi/session_manager.rb +5 -0
  41. data/lib/samagotchi/steer.rb +5 -3
  42. data/lib/samagotchi/terminal_ui/attached_loop.rb +70 -9
  43. data/lib/samagotchi/terminal_ui/between_turns.rb +8 -3
  44. data/lib/samagotchi/terminal_ui/formatting.rb +13 -5
  45. data/lib/samagotchi/terminal_ui/gap_clock.rb +78 -0
  46. data/lib/samagotchi/terminal_ui/reline_seam.rb +20 -2
  47. data/lib/samagotchi/terminal_ui.rb +40 -5
  48. data/lib/samagotchi/tool_activity.rb +11 -0
  49. data/lib/samagotchi/turn_checkpoint.rb +6 -1
  50. data/lib/samagotchi/turn_endings.rb +137 -0
  51. data/lib/samagotchi/version.rb +1 -1
  52. data/lib/samagotchi/web/app.rb +28 -1369
  53. data/lib/samagotchi/web/grants.rb +3 -12
  54. data/lib/samagotchi/web/message_display.rb +274 -0
  55. data/lib/samagotchi/web/participant_view.rb +12 -5
  56. data/lib/samagotchi/web/private_files.rb +31 -0
  57. data/lib/samagotchi/web/public/app.js +41 -9
  58. data/lib/samagotchi/web/public/apple-touch-icon.png +0 -0
  59. data/lib/samagotchi/web/public/bubbles.js +29 -7
  60. data/lib/samagotchi/web/public/format.js +72 -6
  61. data/lib/samagotchi/web/public/icons/chi-180.png +0 -0
  62. data/lib/samagotchi/web/public/icons/chi-192.png +0 -0
  63. data/lib/samagotchi/web/public/icons/chi-512.png +0 -0
  64. data/lib/samagotchi/web/public/index.html +6 -0
  65. data/lib/samagotchi/web/public/manifest.webmanifest +13 -0
  66. data/lib/samagotchi/web/public/notifications.js +137 -3
  67. data/lib/samagotchi/web/public/notify.js +1 -1
  68. data/lib/samagotchi/web/public/participant.html +2 -0
  69. data/lib/samagotchi/web/public/participant.js +30 -7
  70. data/lib/samagotchi/web/public/participant_model.js +25 -2
  71. data/lib/samagotchi/web/public/push.js +95 -0
  72. data/lib/samagotchi/web/public/stage_view.js +8 -0
  73. data/lib/samagotchi/web/public/stream_handlers.js +32 -4
  74. data/lib/samagotchi/web/public/sw.js +101 -0
  75. data/lib/samagotchi/web/public/time_gaps.js +98 -0
  76. data/lib/samagotchi/web/public/tokens.css +2 -2
  77. data/lib/samagotchi/web/public/turn_events.js +25 -1
  78. data/lib/samagotchi/web/public/turn_html.js +15 -8
  79. data/lib/samagotchi/web/public/turn_view.js +8 -3
  80. data/lib/samagotchi/web/push/attention.rb +105 -0
  81. data/lib/samagotchi/web/push/client.rb +103 -0
  82. data/lib/samagotchi/web/push/crypto.rb +126 -0
  83. data/lib/samagotchi/web/push/notifier.rb +230 -0
  84. data/lib/samagotchi/web/push/store.rb +285 -0
  85. data/lib/samagotchi/web/push.rb +73 -0
  86. data/lib/samagotchi/web/push_routes.rb +99 -0
  87. data/lib/samagotchi/web/running.rb +39 -4
  88. data/lib/samagotchi/web/server.rb +112 -32
  89. data/lib/samagotchi/web/session_control.rb +441 -0
  90. data/lib/samagotchi/web/session_create.rb +384 -0
  91. data/lib/samagotchi/web/session_show.rb +411 -0
  92. data/lib/samagotchi/web/tunnel/tailscale.rb +325 -0
  93. data/lib/samagotchi/web/tunnel.rb +133 -0
  94. data/lib/samagotchi/worker.rb +3 -2
  95. metadata +30 -1
@@ -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.109.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