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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +57 -1
- data/bin/chi +5 -1
- data/docs/architecture.md +9 -6
- data/docs/cli.md +43 -1
- data/docs/configuration.md +140 -3
- data/docs/internals/engine.md +58 -14
- data/docs/internals/participants.md +7 -4
- data/docs/internals/prompt-caching.md +5 -0
- data/docs/internals/remote-sessions.md +3 -3
- data/docs/internals/web-push.md +200 -0
- data/docs/internals/working-set.md +7 -6
- data/docs/plugins.md +1 -1
- data/docs/sessions.md +4 -2
- data/docs/sub-agent.md +1 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +4 -0
- data/lib/samagotchi/context_note.rb +7 -3
- data/lib/samagotchi/engine.rb +166 -499
- data/lib/samagotchi/engine_snapshots.rb +223 -0
- data/lib/samagotchi/event_fanout.rb +236 -0
- data/lib/samagotchi/idle_recap.rb +1 -1
- data/lib/samagotchi/idle_reminders.rb +1 -1
- data/lib/samagotchi/kernel_loop.rb +12 -7
- data/lib/samagotchi/llm/chat_loop.rb +9 -2
- data/lib/samagotchi/message_time.rb +104 -0
- data/lib/samagotchi/model_binding.rb +138 -0
- data/lib/samagotchi/reminder_queue.rb +20 -3
- data/lib/samagotchi/reply_wait/poll.rb +39 -1
- data/lib/samagotchi/run_options.rb +4 -1
- data/lib/samagotchi/send_command/wait_target.rb +10 -3
- data/lib/samagotchi/send_command.rb +2 -1
- data/lib/samagotchi/session.rb +12 -3
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +11 -5
- data/lib/samagotchi/session_access/remote.rb +2 -1
- data/lib/samagotchi/session_access.rb +7 -3
- data/lib/samagotchi/session_inbox.rb +7 -3
- data/lib/samagotchi/session_manager.rb +5 -0
- data/lib/samagotchi/steer.rb +5 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +70 -9
- data/lib/samagotchi/terminal_ui/between_turns.rb +8 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +13 -5
- data/lib/samagotchi/terminal_ui/gap_clock.rb +78 -0
- data/lib/samagotchi/terminal_ui/reline_seam.rb +20 -2
- data/lib/samagotchi/terminal_ui.rb +40 -5
- data/lib/samagotchi/tool_activity.rb +11 -0
- data/lib/samagotchi/turn_checkpoint.rb +6 -1
- data/lib/samagotchi/turn_endings.rb +137 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +28 -1369
- data/lib/samagotchi/web/grants.rb +3 -12
- data/lib/samagotchi/web/message_display.rb +274 -0
- data/lib/samagotchi/web/participant_view.rb +12 -5
- data/lib/samagotchi/web/private_files.rb +31 -0
- data/lib/samagotchi/web/public/app.js +41 -9
- data/lib/samagotchi/web/public/apple-touch-icon.png +0 -0
- data/lib/samagotchi/web/public/bubbles.js +29 -7
- data/lib/samagotchi/web/public/format.js +72 -6
- data/lib/samagotchi/web/public/icons/chi-180.png +0 -0
- data/lib/samagotchi/web/public/icons/chi-192.png +0 -0
- data/lib/samagotchi/web/public/icons/chi-512.png +0 -0
- data/lib/samagotchi/web/public/index.html +6 -0
- data/lib/samagotchi/web/public/manifest.webmanifest +13 -0
- data/lib/samagotchi/web/public/notifications.js +137 -3
- data/lib/samagotchi/web/public/notify.js +1 -1
- data/lib/samagotchi/web/public/participant.html +2 -0
- data/lib/samagotchi/web/public/participant.js +30 -7
- data/lib/samagotchi/web/public/participant_model.js +25 -2
- data/lib/samagotchi/web/public/push.js +95 -0
- data/lib/samagotchi/web/public/stage_view.js +8 -0
- data/lib/samagotchi/web/public/stream_handlers.js +32 -4
- data/lib/samagotchi/web/public/sw.js +101 -0
- data/lib/samagotchi/web/public/time_gaps.js +98 -0
- data/lib/samagotchi/web/public/tokens.css +2 -2
- data/lib/samagotchi/web/public/turn_events.js +25 -1
- data/lib/samagotchi/web/public/turn_html.js +15 -8
- data/lib/samagotchi/web/public/turn_view.js +8 -3
- data/lib/samagotchi/web/push/attention.rb +105 -0
- data/lib/samagotchi/web/push/client.rb +103 -0
- data/lib/samagotchi/web/push/crypto.rb +126 -0
- data/lib/samagotchi/web/push/notifier.rb +230 -0
- data/lib/samagotchi/web/push/store.rb +285 -0
- data/lib/samagotchi/web/push.rb +73 -0
- data/lib/samagotchi/web/push_routes.rb +99 -0
- data/lib/samagotchi/web/running.rb +39 -4
- data/lib/samagotchi/web/server.rb +112 -32
- data/lib/samagotchi/web/session_control.rb +441 -0
- data/lib/samagotchi/web/session_create.rb +384 -0
- data/lib/samagotchi/web/session_show.rb +411 -0
- data/lib/samagotchi/web/tunnel/tailscale.rb +325 -0
- data/lib/samagotchi/web/tunnel.rb +133 -0
- data/lib/samagotchi/worker.rb +3 -2
- 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
|