samagotchi 0.155.0 → 0.157.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 +2 -1
- data/docs/architecture.md +22 -13
- data/docs/bundles.md +3 -1
- data/docs/cli.md +9 -8
- data/docs/configuration.md +36 -4
- data/docs/guardrails.md +9 -5
- data/docs/hooks.md +11 -5
- data/docs/internals/anthropic.md +1 -1
- data/docs/internals/attached-context.md +3 -2
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +83 -6
- data/docs/internals/bundles.md +28 -18
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +641 -937
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +2 -2
- data/docs/internals/participants.md +3 -2
- data/docs/internals/prompt-caching.md +4 -1
- data/docs/internals/remote-sessions.md +8 -3
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +2 -2
- data/docs/memory.md +13 -2
- data/docs/plugins.md +35 -2
- data/docs/sessions.md +11 -11
- data/docs/sub-agent.md +17 -2
- data/docs/system-one.md +3 -2
- data/docs/testing.md +53 -2
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +10 -4
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +109 -9
- data/lib/samagotchi/bridge/turn_drawings.rb +265 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/check.rb +18 -0
- data/lib/samagotchi/bundle_command/try/sandbox.rb +2 -2
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +5 -2
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/child_move.rb +13 -57
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +9 -23
- data/lib/samagotchi/config.rb +20 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_sources.rb +5 -4
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/context_judge.rb +19 -11
- data/lib/samagotchi/engine/messages.rb +1 -1
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +29 -11
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine/side_requests.rb +21 -11
- data/lib/samagotchi/engine/toolkit.rb +10 -9
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +7 -7
- data/lib/samagotchi/engine/verify.rb +1 -1
- data/lib/samagotchi/engine.rb +43 -12
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +6 -3
- data/lib/samagotchi/guardrails/targets.rb +24 -9
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +58 -46
- data/lib/samagotchi/kernel_loop.rb +53 -124
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/chat_loop.rb +40 -21
- data/lib/samagotchi/llm/native_backend.rb +1 -1
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- data/lib/samagotchi/llm_context_notice.rb +1 -1
- data/lib/samagotchi/llm_context_strategy.rb +29 -67
- data/lib/samagotchi/llm_context_view.rb +9 -0
- data/lib/samagotchi/memory_bundle/asset_installer.rb +71 -76
- data/lib/samagotchi/memory_bundle/builder.rb +2 -1
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_copy.rb +2 -1
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +8 -1
- data/lib/samagotchi/memory_bundle/installer.rb +185 -169
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +15 -1
- data/lib/samagotchi/memory_bundle/remerge.rb +25 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -6
- data/lib/samagotchi/memory_bundle/status.rb +14 -20
- data/lib/samagotchi/memory_bundle/system_bundle.rb +24 -6
- data/lib/samagotchi/memory_bundle/trash.rb +23 -8
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +7 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +2 -3
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/api.rb +41 -6
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/loader.rb +33 -10
- data/lib/samagotchi/plugin/sessions.rb +3 -6
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +27 -0
- data/lib/samagotchi/reminder_carry.rb +1 -1
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +36 -37
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +370 -445
- data/lib/samagotchi/session_access/local.rb +12 -11
- data/lib/samagotchi/session_access/remote.rb +22 -6
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_field.rb +133 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +139 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +118 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +28 -1541
- data/lib/samagotchi/session_metrics.rb +8 -5
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui/join_replay.rb +86 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +5 -7
- data/lib/samagotchi/testing/install.rb +2 -1
- data/lib/samagotchi/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +10 -4
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +25 -5
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/delegate.rb +55 -45
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +5 -5
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/tools/registry.rb +28 -11
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +52 -51
- data/lib/samagotchi/tools/tool_surface.rb +22 -8
- data/lib/samagotchi/turn_context.rb +2 -1
- data/lib/samagotchi/turn_stages/context_judge.rb +38 -36
- data/lib/samagotchi/turn_stages/objectives.rb +56 -52
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +13 -17
- data/lib/samagotchi/turn_stages/verify_anchors.rb +9 -10
- data/lib/samagotchi/turn_stages/warmup.rb +14 -15
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +45 -28
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +3 -3
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +6 -6
- data/lib/samagotchi/web/session_control.rb +4 -4
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +67 -5
- data/lib/samagotchi/web/session_summary.rb +39 -28
- data/lib/samagotchi/worker.rb +83 -32
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_link.rb +3 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +32 -4
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "../log"
|
|
7
|
+
require_relative "../message_time"
|
|
8
|
+
require_relative "../steer"
|
|
9
|
+
|
|
10
|
+
module Samagotchi
|
|
11
|
+
class Bridge
|
|
12
|
+
# A finished turn as live drew it (its drawing), one JSON line per turn in
|
|
13
|
+
# the session's folder (FILE), so a reload or a reconnect draws the turn
|
|
14
|
+
# the way the live page did: the TurnAccumulator's final fold of the turn
|
|
15
|
+
# (its parts in order: steps, rows, notices, steers, merged lines,
|
|
16
|
+
# reminders, in-turn cards) and the end event's display keys (END_KEYS).
|
|
17
|
+
# Display-side only: no request, message, strategy, recap, --resume or
|
|
18
|
+
# continue reads it.
|
|
19
|
+
#
|
|
20
|
+
# A line names the turn's own messages by the range the Engine put on
|
|
21
|
+
# the end event (`kept_from`/`kept_to`: the turn's messages are
|
|
22
|
+
# `kept[from...to]`) and digests both ends (#digest). It holds while the
|
|
23
|
+
# messages still reach `to` and both ends digest the same: a rollback, a
|
|
24
|
+
# rolled-back failure or a prompt handed back drops it. Of two valid
|
|
25
|
+
# lines that overlap (a rollback, then a re-run that said the same at the
|
|
26
|
+
# same boundaries), and of several for one turn id, the newest wins
|
|
27
|
+
# (.valid). A turn with no range (an older worker, a turn that kept
|
|
28
|
+
# nothing) or an empty one writes no line, and draws as before.
|
|
29
|
+
#
|
|
30
|
+
# Written by the accumulator's on_turn_end hook (#append), inside the
|
|
31
|
+
# SessionObserver's delivery: the line is on disk before any stream gets
|
|
32
|
+
# the end frame. That departs from SessionObserver's "enqueue-only" note,
|
|
33
|
+
# as CardStore's save does: one append per turn end.
|
|
34
|
+
class TurnDrawings
|
|
35
|
+
FILE = "turn_drawings.jsonl"
|
|
36
|
+
# In the sidecar's features (Bridge::FEATURES): this worker's CardStore
|
|
37
|
+
# lists an in-turn entry's of_turn, which a page drawing the turn needs
|
|
38
|
+
# to leave that entry to the drawing. chi web draws no drawings for a
|
|
39
|
+
# live worker without it (an older one: its rows would show twice).
|
|
40
|
+
FEATURE = "turn_drawings"
|
|
41
|
+
# A show draws at most this many turns from their drawings, the newest
|
|
42
|
+
# (the payload and the page's offscreen render stay bounded in a long
|
|
43
|
+
# session); older turns draw as before, from their messages.
|
|
44
|
+
SHOWN_MAX = 100
|
|
45
|
+
VERSION = 1
|
|
46
|
+
# The end event's keys the page draws a finished turn's end from (not
|
|
47
|
+
# the answer: its text, html and refs stay the message's; not the range:
|
|
48
|
+
# the line's from/to).
|
|
49
|
+
END_KEYS = %i[type turn_id duration_ms revises verify cancellation_reason cancelled_by summary message error_class
|
|
50
|
+
kept_steps].freeze
|
|
51
|
+
# What a line keeps of the accumulator's turn.
|
|
52
|
+
DRAWING_KEYS = (%i[prompt turn_id origin continue at images parts] + Steer::AUTHOR_KEYS).uniq.freeze
|
|
53
|
+
KINDS = { "turn_completed" => "completed", "turn_canceled" => "canceled", "turn_failed" => "failed" }.freeze
|
|
54
|
+
|
|
55
|
+
# One line of the file (string-keyed drawing and end, as JSON has them).
|
|
56
|
+
Line = Data.define(:v, :turn_id, :kind, :from, :to, :from_digest, :to_digest, :drawing, :end) do
|
|
57
|
+
# @return [Line, nil] nil for a line this chi can't draw (another
|
|
58
|
+
# version, a missing field)
|
|
59
|
+
def self.from_h(raw)
|
|
60
|
+
return nil unless raw.is_a?(Hash) && raw["v"] == VERSION
|
|
61
|
+
return nil unless raw["turn_id"].is_a?(String) && !raw["turn_id"].empty?
|
|
62
|
+
return nil unless raw["from"].is_a?(Integer) && raw["to"].is_a?(Integer) && raw["from"] < raw["to"] && raw["from"] >= 0
|
|
63
|
+
return nil unless raw["from_digest"].is_a?(String) && raw["to_digest"].is_a?(String)
|
|
64
|
+
return nil unless raw["drawing"].is_a?(Hash) && raw["end"].is_a?(Hash)
|
|
65
|
+
|
|
66
|
+
new(v: raw["v"], turn_id: raw["turn_id"], kind: raw["kind"].to_s, from: raw["from"], to: raw["to"],
|
|
67
|
+
from_digest: raw["from_digest"], to_digest: raw["to_digest"], drawing: raw["drawing"], end: raw["end"])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_h = super.transform_keys(&:to_s)
|
|
71
|
+
|
|
72
|
+
def range = (from...to)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param path [String, nil] the file (nil: nothing is written)
|
|
76
|
+
# @param messages [#call] the session's messages now (the Engine's,
|
|
77
|
+
# as CardStore's): the end step replaced them before the end event, so
|
|
78
|
+
# they hold the turn's range when #append runs
|
|
79
|
+
def initialize(path:, messages:)
|
|
80
|
+
@path = path
|
|
81
|
+
@messages = messages
|
|
82
|
+
@last = path ? self.class.tail_line(path) : nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The newest line this store wrote or started from (P3's snapshot).
|
|
86
|
+
# @return [Line, nil]
|
|
87
|
+
attr_reader :last
|
|
88
|
+
|
|
89
|
+
# The accumulator's hook: the turn's line, when its end event names a
|
|
90
|
+
# range. Never raises (a turn that can't be drawn still ended).
|
|
91
|
+
# @param turn [Hash] the accumulator's final fold (symbol keys)
|
|
92
|
+
# @param end_event [Hash] the end event
|
|
93
|
+
def append(turn, end_event)
|
|
94
|
+
line = line_for(turn, end_event)
|
|
95
|
+
return unless line
|
|
96
|
+
|
|
97
|
+
write(line)
|
|
98
|
+
@last = line
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
Log.warn(:bridge, "turn_drawing_failed", error: "#{e.class}: #{e.message}")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def line_for(turn, end_event)
|
|
104
|
+
from = end_event[:kept_from]
|
|
105
|
+
to = end_event[:kept_to]
|
|
106
|
+
return nil unless @path && turn && from.is_a?(Integer) && to.is_a?(Integer) && from < to && from >= 0
|
|
107
|
+
|
|
108
|
+
messages = @messages.call || []
|
|
109
|
+
return nil if messages.size < to
|
|
110
|
+
|
|
111
|
+
id = end_event[:turn_id] || turn[:turn_id]
|
|
112
|
+
return nil unless id
|
|
113
|
+
|
|
114
|
+
Line.new(v: VERSION, turn_id: id.to_s, kind: KINDS[end_event[:type].to_s] || end_event[:type].to_s, from: from, to: to,
|
|
115
|
+
from_digest: self.class.digest(messages[from]), to_digest: self.class.digest(messages[to - 1]),
|
|
116
|
+
drawing: JSON.parse(JSON.generate(scrubbed(turn.slice(*DRAWING_KEYS)))),
|
|
117
|
+
end: JSON.parse(JSON.generate(scrubbed(self.class.end_of(end_event)))))
|
|
118
|
+
end
|
|
119
|
+
private :line_for
|
|
120
|
+
|
|
121
|
+
# Newline-first: a torn last line (a crash mid-write) ends before this
|
|
122
|
+
# one starts, so both parse apart; then one write.
|
|
123
|
+
def write(line)
|
|
124
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
125
|
+
File.open(@path, File::RDWR | File::CREAT | File::APPEND, 0o600) do |file|
|
|
126
|
+
size = file.size
|
|
127
|
+
torn = size.positive? && (file.pread(1, size - 1) != "\n")
|
|
128
|
+
file.write("#{"\n" if torn}#{JSON.generate(line.to_h)}\n")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
private :write
|
|
132
|
+
|
|
133
|
+
def scrubbed(value) = self.class.scrub(value)
|
|
134
|
+
private :scrubbed
|
|
135
|
+
|
|
136
|
+
class << self
|
|
137
|
+
# The end event's display keys (END_KEYS), with an empty answer's
|
|
138
|
+
# notice from its summary.
|
|
139
|
+
# @return [Hash]
|
|
140
|
+
def end_of(event)
|
|
141
|
+
out = event.slice(*END_KEYS).compact
|
|
142
|
+
out[:type] = out[:type].to_s if out[:type]
|
|
143
|
+
empty = event.dig(:turn_summary, :empty_answer)
|
|
144
|
+
out[:turn_summary] = { empty_answer: empty } if empty
|
|
145
|
+
out
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A message's digest in one form for the live (symbol keys, raw
|
|
149
|
+
# strings) and the saved (string keys, scrubbed as Session#to_h does)
|
|
150
|
+
# messages: role, content and time, each read from either key.
|
|
151
|
+
# @return [String]
|
|
152
|
+
def digest(message)
|
|
153
|
+
return "" unless message.is_a?(Hash)
|
|
154
|
+
|
|
155
|
+
role = fetch(message, :role)
|
|
156
|
+
time = fetch(message, MessageTime::KEY)
|
|
157
|
+
Digest::SHA1.hexdigest(JSON.generate(scrub([role&.to_s, fetch(message, :content), time&.to_s])))
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Every String in +value+ as Session#to_h saves it (UTF-8, invalid
|
|
161
|
+
# bytes as "?"); symbol keys stay symbols (JSON writes them alike).
|
|
162
|
+
def scrub(value)
|
|
163
|
+
case value
|
|
164
|
+
when String
|
|
165
|
+
text = value.encoding == Encoding::UTF_8 ? value : value.dup.force_encoding(Encoding::UTF_8)
|
|
166
|
+
text.valid_encoding? ? text : text.scrub("?")
|
|
167
|
+
when Array then value.map { |item| scrub(item) }
|
|
168
|
+
when Hash then value.to_h { |key, item| [key, scrub(item)] }
|
|
169
|
+
when Symbol then value.to_s
|
|
170
|
+
else value
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The session folder's lines, oldest first (unreadable, torn and
|
|
175
|
+
# unknown lines skipped), cached by the file's path, size and mtime
|
|
176
|
+
# (chi web reads it on each full show).
|
|
177
|
+
# @return [Array<Line>]
|
|
178
|
+
def read(session_dir)
|
|
179
|
+
path = File.join(session_dir, FILE)
|
|
180
|
+
stat = File.stat(path)
|
|
181
|
+
key = [stat.size, stat.mtime.to_r]
|
|
182
|
+
cached = cache_mutex.synchronize { cache[path] }
|
|
183
|
+
return cached[1] if cached && cached[0] == key
|
|
184
|
+
|
|
185
|
+
lines = parse(File.binread(path))
|
|
186
|
+
cache_mutex.synchronize do
|
|
187
|
+
cache.shift while cache.size >= CACHE_SIZE
|
|
188
|
+
cache[path] = [key, lines]
|
|
189
|
+
end
|
|
190
|
+
lines
|
|
191
|
+
rescue SystemCallError, IOError
|
|
192
|
+
[]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# @return [Array<Line>]
|
|
196
|
+
def parse(text)
|
|
197
|
+
text.dup.force_encoding(Encoding::UTF_8).scrub("?").each_line.filter_map do |raw|
|
|
198
|
+
Line.from_h(JSON.parse(raw))
|
|
199
|
+
rescue JSON::ParserError
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# The lines that hold against +messages+ (Session messages, symbol
|
|
205
|
+
# or string keys): each one's range still in them with both ends the
|
|
206
|
+
# same; of overlapping ranges and of one turn's lines the newest
|
|
207
|
+
# (last in the file) wins. Ordered by their range.
|
|
208
|
+
# @return [Array<Line>]
|
|
209
|
+
def valid(lines, messages)
|
|
210
|
+
messages = Array(messages)
|
|
211
|
+
kept = []
|
|
212
|
+
lines.reverse_each do |line|
|
|
213
|
+
next unless holds?(line, messages)
|
|
214
|
+
next if kept.any? { |other| other.turn_id == line.turn_id || overlap?(other, line) }
|
|
215
|
+
|
|
216
|
+
kept << line
|
|
217
|
+
end
|
|
218
|
+
kept.sort_by(&:from)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# The last parseable line of +path+, or nil.
|
|
222
|
+
# @return [Line, nil]
|
|
223
|
+
def tail_line(path)
|
|
224
|
+
return nil unless File.file?(path)
|
|
225
|
+
|
|
226
|
+
File.open(path, "rb") do |file|
|
|
227
|
+
size = file.size
|
|
228
|
+
chunk = file.pread([size, TAIL_BYTES].min, [size - TAIL_BYTES, 0].max)
|
|
229
|
+
parse(chunk.to_s).last
|
|
230
|
+
end
|
|
231
|
+
rescue SystemCallError, IOError
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Whether +line+ is small enough to send in a snapshot: within what
|
|
236
|
+
# #tail_line reads back after a restart (TAIL_BYTES), so a join draws
|
|
237
|
+
# a turn the same before and after one (a bigger line: rebuilt).
|
|
238
|
+
def sendable?(line) = JSON.generate(line.to_h).bytesize <= TAIL_BYTES
|
|
239
|
+
|
|
240
|
+
def holds?(line, messages)
|
|
241
|
+
messages.size >= line.to && digest(messages[line.from]) == line.from_digest &&
|
|
242
|
+
digest(messages[line.to - 1]) == line.to_digest
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def overlap?(one, other) = one.from < other.to && other.from < one.to
|
|
246
|
+
|
|
247
|
+
private
|
|
248
|
+
|
|
249
|
+
def fetch(message, key)
|
|
250
|
+
message.key?(key) ? message[key] : message[key.to_s]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def cache = (@cache ||= {})
|
|
254
|
+
def cache_mutex = (@cache_mutex ||= Mutex.new)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Sessions whose parsed file chi web keeps (a long one's is tens of MB
|
|
258
|
+
# of objects; the mtime moves at every turn end anyway).
|
|
259
|
+
CACHE_SIZE = 8
|
|
260
|
+
# Read from the end at start (#last): a drawing is a few KB, its outputs cut.
|
|
261
|
+
TAIL_BYTES = 1024 * 1024
|
|
262
|
+
private_constant :CACHE_SIZE, :TAIL_BYTES
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -21,6 +21,7 @@ require_relative "bridge/pending_card"
|
|
|
21
21
|
require_relative "bridge/ring_buffer"
|
|
22
22
|
require_relative "bridge/sse_writer"
|
|
23
23
|
require_relative "bridge/turn_accumulator"
|
|
24
|
+
require_relative "bridge/turn_drawings"
|
|
24
25
|
require_relative "session"
|
|
25
26
|
require_relative "engine"
|
|
26
27
|
require_relative "system_one/log"
|
|
@@ -134,7 +135,10 @@ module Samagotchi
|
|
|
134
135
|
@bind = bind
|
|
135
136
|
@port = port
|
|
136
137
|
@ring = RingBuffer.new(capacity: ring_capacity)
|
|
137
|
-
|
|
138
|
+
# A finished turn's drawing (TurnDrawings), written by the accumulator
|
|
139
|
+
# at the end event, before any stream gets it.
|
|
140
|
+
@drawings = TurnDrawings.new(path: File.join(session_dir, TurnDrawings::FILE), messages: -> { @engine.session&.messages })
|
|
141
|
+
@accumulator = TurnAccumulator.new(on_turn_end: @drawings.method(:append))
|
|
138
142
|
# Saved in the session's folder: a later worker, and the web without
|
|
139
143
|
# one, show the cards where they were.
|
|
140
144
|
@cards = CardStore.new(path: File.join(session_dir, CardStore::FILE), messages: -> { @engine.session&.messages })
|
|
@@ -300,7 +304,7 @@ module Samagotchi
|
|
|
300
304
|
# the event log: the Engine's messages (not the lagging copy on disk),
|
|
301
305
|
# the turn in progress, turns queued behind it, the idle recap since the
|
|
302
306
|
# last turn, the recap saved with the session (also from before the last
|
|
303
|
-
# turns: {text:, covered:, turns_since:, created_at:}), a pending continue offer, the guardrail and plugin load warnings,
|
|
307
|
+
# turns: {text:, covered:, turns_since:, opening_id:, created_at:}), a pending continue offer, the guardrail and plugin load warnings,
|
|
304
308
|
# the plugins' init tasks still running (Engine#init_tasks), the
|
|
305
309
|
# last cards and between-turns notices (CardStore#list), the session's commands (Commands::Registry#listing:
|
|
306
310
|
# what an attached TUI routes and completes, the web's autocomplete), and the event_seq it all covers.
|
|
@@ -315,13 +319,20 @@ module Samagotchi
|
|
|
315
319
|
# working_set: the running turn's last :working_set_updated ({items:,
|
|
316
320
|
# at:}, TurnAccumulator#working_set), which the messages lag; nil
|
|
317
321
|
# between turns and before the turn emitted one.
|
|
322
|
+
# last_turn_drawing: the last exchange's turn as live drew it
|
|
323
|
+
# ({drawing:, end:}, TurnDrawings#last, not unfolded: an attaching TUI
|
|
324
|
+
# unfolds it, TurnAccumulator.replay_events), only while it holds against
|
|
325
|
+
# these messages and ends at their end; absent otherwise (a rollback, a
|
|
326
|
+
# later Continue or note, an older worker's file).
|
|
318
327
|
# @return [Hash] {messages:, current_turn:, working_set:, queued:, queued_commands:, recap:, saved_recap:, continue_offer:, pending_question:,
|
|
319
|
-
# guardrail_warning:, plugin_warning:, init_tasks:, cards:, commands:, chi_version:, event_seq:, event_id:}
|
|
328
|
+
# guardrail_warning:, plugin_warning:, init_tasks:, cards:, commands:, chi_version:, event_seq:, event_id:, last_turn_drawing:}
|
|
320
329
|
def snapshot
|
|
321
330
|
@engine.synchronize_events do
|
|
322
331
|
seq = @engine.event_count
|
|
332
|
+
messages = @engine.messages_checkpoint
|
|
333
|
+
drawing = last_turn_drawing(messages)
|
|
323
334
|
{
|
|
324
|
-
messages:
|
|
335
|
+
messages: messages,
|
|
325
336
|
current_turn: current_turn_with_generation,
|
|
326
337
|
working_set: @accumulator.working_set,
|
|
327
338
|
queued: @accumulator.queued,
|
|
@@ -338,10 +349,24 @@ module Samagotchi
|
|
|
338
349
|
chi_version: Samagotchi::VERSION,
|
|
339
350
|
event_seq: seq,
|
|
340
351
|
event_id: event_id(seq)
|
|
341
|
-
}
|
|
352
|
+
}.merge(drawing ? { last_turn_drawing: drawing } : {})
|
|
342
353
|
end
|
|
343
354
|
end
|
|
344
355
|
|
|
356
|
+
# The newest drawing (TurnDrawings#last) as the snapshot sends it, when
|
|
357
|
+
# it is the last exchange's: its range still holds against +messages+
|
|
358
|
+
# and ends at their end, and is no bigger than a restart reads back
|
|
359
|
+
# (TurnDrawings.sendable?). nil otherwise.
|
|
360
|
+
# @return [Hash, nil] {drawing:, end:} (string keys inside, as the file)
|
|
361
|
+
def last_turn_drawing(messages)
|
|
362
|
+
line = @drawings.last
|
|
363
|
+
return nil unless line && line.to == messages.size && TurnDrawings.holds?(line, messages)
|
|
364
|
+
return nil unless TurnDrawings.sendable?(line)
|
|
365
|
+
|
|
366
|
+
{ drawing: line.drawing, end: line.end }
|
|
367
|
+
end
|
|
368
|
+
private :last_turn_drawing
|
|
369
|
+
|
|
345
370
|
# The snapshot and the session state (status, pending question, ...) as
|
|
346
371
|
# one step of the event log, both as of the same event_seq: what a
|
|
347
372
|
# snapshot or reset frame (SSEWriter) and GET snapshot carry.
|
|
@@ -664,7 +689,7 @@ module Samagotchi
|
|
|
664
689
|
# review turn, and chi web says so (Delivery::PAIR_FEATURE).
|
|
665
690
|
FEATURES = ["restart", "task_stop", "reparent", Delivery::QUEUE_FEATURE, ReplyWait::TURN_IDS_FEATURE,
|
|
666
691
|
VerifyAnchor::FEATURE, ReviseAnchor::FEATURE,
|
|
667
|
-
SessionInbox::UNQUEUE_FEATURE, Delivery::REVIEW_FEATURE, Delivery::PAIR_FEATURE].freeze
|
|
692
|
+
SessionInbox::UNQUEUE_FEATURE, Delivery::REVIEW_FEATURE, Delivery::PAIR_FEATURE, TurnDrawings::FEATURE].freeze
|
|
668
693
|
|
|
669
694
|
# The routes, by method and what follows /session/:id/: a handler takes
|
|
670
695
|
# the session id and the request body and returns [headers, status, body].
|
|
@@ -29,12 +29,12 @@ module Samagotchi
|
|
|
29
29
|
# @return [Array<Recipient>] newest first
|
|
30
30
|
def list(state_dir:, active_hours:, include_tests: Session.test_session_env?, now: Time.now)
|
|
31
31
|
cutoff = now - (active_hours.to_f * 3600)
|
|
32
|
-
SessionManager.
|
|
33
|
-
next if row
|
|
34
|
-
next unless row
|
|
32
|
+
SessionManager.session_rows(include_tests: include_tests, state_dir: state_dir).filter_map do |row|
|
|
33
|
+
next if row.delegate || row.scratch
|
|
34
|
+
next unless row.live || row.owner == "tui" || active_since?(row, cutoff, state_dir)
|
|
35
35
|
|
|
36
|
-
Recipient.new(id: row
|
|
37
|
-
desc: row
|
|
36
|
+
Recipient.new(id: row.id, short_id: row.short_id, project: row.project, cwd: row.cwd,
|
|
37
|
+
desc: row.desc, live: row.live, owner: row.owner)
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -43,11 +43,11 @@ module Samagotchi
|
|
|
43
43
|
# session with no turn recorded; it is never older than the last
|
|
44
44
|
# turn's end, so an older one rules the session out unread.
|
|
45
45
|
def active_since?(row, cutoff, state_dir)
|
|
46
|
-
return false unless (time(row
|
|
46
|
+
return false unless (time(row.updated_at) || Time.at(0)) >= cutoff
|
|
47
47
|
|
|
48
|
-
session = Session.summary_from_file(Session.session_file(row
|
|
48
|
+
session = Session.summary_from_file(Session.session_file(row.id, state_dir: state_dir))
|
|
49
49
|
ended = time(session&.last_turn&.ended_at)
|
|
50
|
-
(ended || time(row
|
|
50
|
+
(ended || time(row.updated_at)) >= cutoff
|
|
51
51
|
end
|
|
52
52
|
private_class_method :active_since?
|
|
53
53
|
|
|
@@ -29,7 +29,7 @@ module Samagotchi
|
|
|
29
29
|
# @param get [#call] a Config key → its value
|
|
30
30
|
# @return [Choice]
|
|
31
31
|
def resolve(host_registry: nil, get: ->(key) { Config.get(key) })
|
|
32
|
-
registry = host_registry || HostRegistry.
|
|
32
|
+
registry = host_registry || HostRegistry.current
|
|
33
33
|
SETTINGS.each do |model, host_ref, base_url|
|
|
34
34
|
target = IdleTarget.resolve(model: get.call(model), host_ref: get.call(host_ref), base_url: get.call(base_url),
|
|
35
35
|
host_registry: registry)
|
|
@@ -348,11 +348,11 @@ module Samagotchi
|
|
|
348
348
|
# The full id +given+ names (an id or the start of one), or nil after
|
|
349
349
|
# saying why there is none.
|
|
350
350
|
def resolve_session(given)
|
|
351
|
-
id = Session.
|
|
352
|
-
Session.load(id, state_dir: @state_dir) #
|
|
351
|
+
id = Session.resolve!(given, state_dir: @state_dir)
|
|
352
|
+
Session.load(id, state_dir: @state_dir) # a file that won't load is refused here too
|
|
353
353
|
id
|
|
354
354
|
rescue ArgumentError => e
|
|
355
|
-
error_line("chi broadcast: #{e.
|
|
355
|
+
error_line("chi broadcast: #{e.message}")
|
|
356
356
|
nil
|
|
357
357
|
rescue SystemCallError => e
|
|
358
358
|
error_line("chi broadcast: session #{given} can't be read: #{e.message}")
|
|
@@ -91,6 +91,7 @@ module Samagotchi
|
|
|
91
91
|
check_settings(manifest) unless @installed
|
|
92
92
|
check_install(manifest) unless missing || placeholders? || manifest.session_settings_clash
|
|
93
93
|
check_api(manifest)
|
|
94
|
+
check_fail_closed(manifest) unless @installed
|
|
94
95
|
check_boundary(manifest)
|
|
95
96
|
check_tests unless @installed
|
|
96
97
|
end
|
|
@@ -439,6 +440,23 @@ module Samagotchi
|
|
|
439
440
|
end
|
|
440
441
|
end
|
|
441
442
|
|
|
443
|
+
# A chi.on(..., on_error: :fail_closed) guard denies the call it raises
|
|
444
|
+
# on; only the manifest's plugin: required: true makes its failing to
|
|
445
|
+
# load deny every call (chi knows it before the plugin registers).
|
|
446
|
+
FAIL_CLOSED = /on_error:\s*(?::fail_closed\b|["']fail_closed["'])/
|
|
447
|
+
|
|
448
|
+
def check_fail_closed(manifest)
|
|
449
|
+
file = manifest.plugin && manifest.plugin[:file]
|
|
450
|
+
return if !file || manifest.plugin[:required] || !File.file?(path(file))
|
|
451
|
+
|
|
452
|
+
source = File.read(path(file))
|
|
453
|
+
return unless (index = source =~ FAIL_CLOSED)
|
|
454
|
+
|
|
455
|
+
add(:warn, :"fail-closed", file, "a fail-closed chi.on hook, but the manifest's plugin: has no required: true: " \
|
|
456
|
+
"if the plugin fails to load, tool calls still run",
|
|
457
|
+
line: source[0, index].count("\n") + 1, fix: "add required: true under plugin: in manifest.yml")
|
|
458
|
+
end
|
|
459
|
+
|
|
442
460
|
def check_boundary(manifest)
|
|
443
461
|
require_relative "../testing/boundary"
|
|
444
462
|
known = shipped? ? Testing::Boundary::EXCEPTIONS.fetch(manifest.name, {}) : {}
|
|
@@ -130,7 +130,7 @@ module Samagotchi
|
|
|
130
130
|
|
|
131
131
|
# Stops what the run started that outlives its child chi: the
|
|
132
132
|
# workers of its delegates (they leave the child's process tree,
|
|
133
|
-
#
|
|
133
|
+
# WorkerBoot.detach_from_spawner) and its tasks (a group of
|
|
134
134
|
# their own each, reparented once the child exits). Found by their
|
|
135
135
|
# records in the sandbox, so nothing outside it: a session's lock
|
|
136
136
|
# held by a group leader, a running task's record whose pid still
|
|
@@ -186,7 +186,7 @@ module Samagotchi
|
|
|
186
186
|
end
|
|
187
187
|
|
|
188
188
|
def leftover_workers
|
|
189
|
-
sessions = File.join(state_home,
|
|
189
|
+
sessions = File.join(state_home, SessionPaths::STATE_SUBDIR)
|
|
190
190
|
Dir.glob(File.join(sessions, "*", OwnerLock::FILE)).filter_map do |lock|
|
|
191
191
|
pid = OwnerLock.owner(File.dirname(lock))&.pid
|
|
192
192
|
pid if ProcessGroup.leader?(pid)
|
|
@@ -28,7 +28,7 @@ module Samagotchi
|
|
|
28
28
|
def read(exit_code:, answer:, stderr:, sandbox:, timed_out: false)
|
|
29
29
|
return empty_trace(exit_code, answer, stderr, sandbox, timed_out) unless @session_id
|
|
30
30
|
|
|
31
|
-
sessions_dir = File.join(@state_home,
|
|
31
|
+
sessions_dir = File.join(@state_home, SessionPaths::STATE_SUBDIR)
|
|
32
32
|
session_dir = Session.session_dir(@session_id, state_dir: sessions_dir)
|
|
33
33
|
session = json_file(Session.session_file(@session_id, state_dir: sessions_dir)) || {}
|
|
34
34
|
messages = Array(session["messages"]).select { |message| message.is_a?(Hash) }
|
|
@@ -639,9 +639,12 @@ module Samagotchi
|
|
|
639
639
|
0
|
|
640
640
|
end
|
|
641
641
|
|
|
642
|
+
# The base and the file; a marked one also the bundle's text it stays
|
|
643
|
+
# behind (its source-less FileState says which).
|
|
642
644
|
def diff_memory(prov, target_dir, key)
|
|
643
|
-
|
|
644
|
-
|
|
645
|
+
state = Samagotchi::MemoryBundle::FileState.of(key, record: prov.record, provenance: prov, target_dir: target_dir, source_dir: nil)
|
|
646
|
+
print_base_and_current(key, state.paths[:base], state.paths[:current])
|
|
647
|
+
print_incoming(prov, key) if state.marked?
|
|
645
648
|
end
|
|
646
649
|
|
|
647
650
|
# The bundle's text a file stays behind (Provenance#incoming_path), so the
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: known-names
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.2.0
|
|
4
4
|
requires_chi: ">= 0.150.0"
|
|
5
5
|
scope: system
|
|
6
6
|
description: Catches a near-miss of a protected name (home folder, login, git name, repo name, names from config) in a tool call and rejects it with the right spelling; mode ask / correct optional
|
|
7
7
|
trust_level: reviewed
|
|
8
8
|
files:
|
|
9
9
|
known_names.md: sha256:d5348b95b6c655017b541910b36e384220da280818160201f6ec9a50e1e4287c
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
event: before_tool_call
|
|
14
|
-
on_error: log
|
|
15
|
-
priority: 50
|
|
10
|
+
plugin:
|
|
11
|
+
file: plugin.rb
|
|
12
|
+
sha256: sha256:d140612e6c34a9d2d205bf818a2a0797ad1e133829481f354aaa9e56698017e3
|
|
16
13
|
# config.yml bundles: known-names: (another key warns; an older chi ignores this)
|
|
17
14
|
settings: [names, ignore, mode, derive, min_length, max_distance]
|
|
@@ -243,3 +243,20 @@ class KnownNames
|
|
|
243
243
|
Array(value).map { |name| name.to_s.strip }.reject(&:empty?)
|
|
244
244
|
end
|
|
245
245
|
end
|
|
246
|
+
|
|
247
|
+
# What chi loads: one KnownNames, built once from config.yml
|
|
248
|
+
# `bundles: known-names:` when the plugin loads, on before_tool_call at
|
|
249
|
+
# priority 50. A raise in the handler is logged and the call goes on
|
|
250
|
+
# (chi.on's log policy); KnownNames#call turns its own errors into a
|
|
251
|
+
# notice first.
|
|
252
|
+
class Plugin
|
|
253
|
+
PRIORITY = 50
|
|
254
|
+
|
|
255
|
+
def initialize(settings = {})
|
|
256
|
+
@names = KnownNames.new(settings)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def register(chi)
|
|
260
|
+
chi.on(:before_tool_call, priority: PRIORITY) { |event, _ctx| @names.call(event) }
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: source-links
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.7.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Links source refs (JIRA tickets, GitHub issues, …) in the model's answer — inline links in the web, refs in a waiting question's reply, and a one-line note after the turn (note false turns it off); sources are configured patterns
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
requires_chi: ">= 0.150.0"
|
|
8
8
|
files:
|
|
9
9
|
source_links.md: sha256:ace01a0f7c308a0ae54ece714c31c0e6d7a49c372d98111030e6d268320bed3a
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
events: [after_turn, question_asked]
|
|
14
|
-
on_error: log
|
|
15
|
-
priority: 90
|
|
10
|
+
plugin:
|
|
11
|
+
file: plugin.rb
|
|
12
|
+
sha256: sha256:b4a777d8b6135f80e248dd21b5370c91dfcf2aea17057734e77241eb9cc4afe4
|
|
16
13
|
# config.yml bundles: source-links: (another key warns; an older chi ignores this)
|
|
17
14
|
settings: [sources, max, note]
|
|
@@ -59,10 +59,10 @@ require "samagotchi/ref"
|
|
|
59
59
|
# action, "Open in <name>", the ref's URL. A URL that isn't http(s) stays a
|
|
60
60
|
# markdown link only. The web's ref menu shows the action.
|
|
61
61
|
#
|
|
62
|
-
# The hook is on :question_asked too (
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
62
|
+
# The hook is on :question_asked too (Plugin below hooks both events): while
|
|
63
|
+
# a question of the model's (or an approval of its call) waits, the refs in
|
|
64
|
+
# the reply that asks it, and in the question's text and options, are refs
|
|
65
|
+
# on that reply, the same as an answer's. The web pairs
|
|
66
66
|
# them in the stage's asking slot, on the question's card and on that step
|
|
67
67
|
# in the history (a ref only in the question's text pairs there alone). No
|
|
68
68
|
# links in the text and no note line for it: the turn's end has those.
|
|
@@ -657,3 +657,22 @@ class SourceLinks
|
|
|
657
657
|
value.to_s.gsub(/[^A-Za-z0-9\-._~]/) { |c| c.bytes.map { |b| format("%%%02X", b) }.join }
|
|
658
658
|
end
|
|
659
659
|
end
|
|
660
|
+
|
|
661
|
+
# What chi loads: one SourceLinks, built once from config.yml
|
|
662
|
+
# `bundles: source-links:` when the plugin loads, on after_turn and
|
|
663
|
+
# question_asked at priority 90 (its state, the remotes it asked git for,
|
|
664
|
+
# is shared). A raise in the handler is logged and the turn goes on
|
|
665
|
+
# (chi.on's log policy).
|
|
666
|
+
class Plugin
|
|
667
|
+
PRIORITY = 90
|
|
668
|
+
|
|
669
|
+
def initialize(settings = {})
|
|
670
|
+
@links = SourceLinks.new(settings)
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
def register(chi)
|
|
674
|
+
%i[after_turn question_asked].each do |event|
|
|
675
|
+
chi.on(event, priority: PRIORITY) { |payload, _ctx| @links.call(payload) }
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
end
|
|
@@ -55,8 +55,8 @@ Only the nested form is read. An env name used as a top-level key (`SAMAGOTCHI_D
|
|
|
55
55
|
**Excluded maps** (YAML-only, not part of the flat registry; skipped by scalar loader):
|
|
56
56
|
|
|
57
57
|
- `model_aliases:` map of alias → model id (`ModelRef.parse`, through `HostRegistry#resolve`). Keys lowercased on write (`ConfigFile.write_model_alias!`). Values may be bare `model` or qualified `host:model` (hybrid; only `:` names a host, `org/model` is an id). Aliases apply once: never point an alias at another alias.
|
|
58
|
-
- `hosts:` map of `name → {host, port | url, api, api_key_env, auth, dialect, profile, first_token_timeout, vision, sampling, thinking, enabled, models}` (`
|
|
59
|
-
- `models:` map of model id or alias → `{profile, vision, sampling, thinking, reasoning_history}` (`
|
|
58
|
+
- `hosts:` map of `name → {host, port | url, api, api_key_env, auth, dialect, profile, first_token_timeout, vision, sampling, thinking, enabled, models}` (`HostConfig.all`, `host_registry.rb` `HostEntry`). Names lowercased; `url:` (http/https, optional path) replaces host/port, never both; `api_key_env:` names the env var holding the API key (never write a key into config.yml); `auth:` (`bearer|x-api-key`) is the header the key goes in (default `x-api-key` on `api: anthropic`, Bearer elsewhere); `dialect:` (`fireworks`, or `standard` for the default; `api: anthropic` only) adjusts the Messages wire for that provider (Bearer by default, no thinking `display`, no `cache_control`, `x-session-affinity`; `LLM::Dialect`, docs/configuration.md "Anthropic Messages hosts"); `first_token_timeout` (seconds, `0` = off; a negative or non-number warns and is ignored) overrides `server.first_token_timeout` for that host; `models:` (a map of model ids, or a plain list) declares ids the host serves whatever its `/v1/models` lists: no "doesn't list" warning, and a bare id routes there as if listed (`HostModel`); an entry may hold `price: {input, output, cache_read?, cache_write?}` in USD per 1M tokens (`ModelPrice`; an unset cache rate is the input rate), which estimates a cost the provider doesn't report or reports as 0 (`cost_estimate`, shown as `~$`), and `served: [ids] | any`, the models a gateway may answer that id with (no served-model warning for them); model settings stay in the root `models:`; a worker reads `hosts:` from the file as it starts (no environment variable sets hosts).
|
|
59
|
+
- `models:` map of model id or alias → `{profile, vision, sampling, thinking, reasoning_history}` (`ModelSettings.all`). `thinking:` (here, on a host, or `thinking.level` for every model) is `off|low|medium|high|default` (`Thinking`; unquoted `off` works, `on` is not a level; `default` = send nothing). Precedence: the session's own level (`/thinking LEVEL`, saved in the session; `chi --thinking` on a session run, `chi send --new --thinking`) > `chi web --thinking`/`SAMAGOTCHI_THINKING_LEVEL` > `models:` > `hosts.<name>.thinking` > `thinking.level` in config.yml > `default`. A `sampling:` key wins over the fields a level sends (`null` drops one); `docs/configuration.md` "Thinking". `reasoning_history:` (here or on a host) is `drop|turn|all`, which of its reasoning a chat host's model gets back. Precedence: the session's own level (`/reasoning-history LEVEL`, saved in the session) > `SAMAGOTCHI_REASONING_HISTORY` > `models:` > `hosts.<name>.reasoning_history` > `turn`; `docs/configuration.md` "Reasoning history". `/thinking` and `/reasoning-history` ask before a change that restarts the prompt cache; `--yes` applies it (`chi send` commands apply directly). `sampling:` (here or on a host) is a map of request fields passed to the provider as written (`temperature`, `top_p`, `presence_penalty`, `repeat_penalty`, …; a model's fields win over its host's per field; `null` = don't send; chi's own fields like `max_tokens`/`stream` are refused with a warning; `docs/configuration.md` "Sampling"). Keys match case-insensitively. `profile` (here or on a host) is `qwen36|gemma4`: the raw prompt format for native hosts. Precedence: `--profile`/`SAMAGOTCHI_MODEL_PROFILE` > `models:` > `hosts.<name>.profile` > the llama.cpp server's chat template > the name (`qwen`/`gemma`) > `qwen36` (`ModelProfile.resolve`). Set one when a model's name hides its family (e.g. a Qwen fine-tune under another name whose server publishes no template).
|
|
60
60
|
- `hooks:` map of `hooks_dir` + per-event lists `{path, on_error}` (`Hooks::Loader.load`). `hooks_dir` may start with `~`.
|
|
61
61
|
- `guardrails:` tool-call rules (`docs/guardrails.md`; read in `Engine#guardrail_rules`, parsed by `Guardrails::Rules.parse`): `enabled` (bool, default true; `false` drops rules and hooks' asks, a deny still applies; config.yml only, like every `guardrails` setting: no env variable), `parent_answers` (`soft`, the default: a parent agent's `chi answer` may deny, answer, Continue and give "Allow once"; `standard`: no allow; `hard`: deny, Stop and plain answers only; never a wider scope; replaces `parent_approvals` and `turn.parent_continue`; a convention for an honest parent, not a security boundary, and the user's choice: set it only when the user asks), `rules:` (list), `disable:` (a rule id or a list of them, `id` or `bundle:id`, switching off a bundle's or config rule without editing it), `small_models` (`auto`, the default: small = 32B or less read from the name; a glob or list of globs; `[]` for none). A rule takes only `id`, `tool`, `command`, `path`, `git`, `models`, `verdict`, `reason`, `scopes`: `id` required; at least one of `tool` (a name, `shell` = execute + task_create, a `File.fnmatch` glob like `"mcp_*"`, or a list), `command` (a Ruby regex on the shell command), `path` (a glob, or `outside_repo`: outside the session's repo, tmp folders and memory files excepted), `git` (only `outside_repo`: a shell call runs commit/add/reset/… in another checkout); `models` (optional: `small`, a model-name glob, or a list; without it the rule is for every model); `verdict` `ask|deny`; `scopes` (for `ask`) a subset of `once, session, repo, rule`. **Any parse error (an unknown key, a bad regex, no verdict) makes chi deny every tool call** until fixed, so validate with `YAML.safe_load` and keep the list shape. Rules are read again on the next tool call after config.yml or a bundle's rules change: no restart needed. Installed bundles' rule files (`chi bundle install guardrails`) add to them; `/guardrails` lists what loaded.
|
|
62
62
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.157.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
8
|
self_map.md: sha256:709bd52710bf6c50334a89a10742ea1554c5985cc2d7c4e7571e526cc4479526
|
|
9
|
-
config_modification_protocol.md: sha256:
|
|
10
|
-
memory_guide.md: sha256:
|
|
9
|
+
config_modification_protocol.md: sha256:25ed2d4901f00eeea226db5eb2b75a05d6cdee4015ac8daa4bed624f745c3922
|
|
10
|
+
memory_guide.md: sha256:fe4dbb25ce4c9f9b51507a6130fa7bf992a2cc41af2453830eacd27c09a65d1e
|
|
11
11
|
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|