samagotchi 0.153.0 → 0.154.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 +28 -1
- data/README.md +2 -3
- data/bin/chi +23 -24
- data/docs/architecture.md +30 -33
- data/docs/broadcast.md +5 -5
- data/docs/bundles.md +9 -12
- data/docs/cli.md +80 -96
- data/docs/configuration.md +27 -26
- data/docs/context.md +3 -3
- data/docs/desktop.md +1 -1
- data/docs/guardrails.md +8 -10
- data/docs/hooks.md +9 -9
- data/docs/internals/attached-context.md +2 -2
- data/docs/internals/background-tasks.md +3 -4
- data/docs/internals/block-renderers.md +4 -0
- data/docs/internals/bridge.md +3 -3
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +33 -7
- data/docs/internals/objectives.md +6 -5
- data/docs/internals/participants.md +7 -7
- data/docs/internals/prompt-caching.md +1 -1
- data/docs/internals/remote-sessions.md +7 -7
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +2 -2
- data/docs/plugins.md +23 -28
- data/docs/sessions.md +24 -24
- data/docs/sub-agent.md +2 -2
- data/docs/system-one.md +1 -1
- data/docs/testing.md +16 -6
- data/lib/samagotchi/answer_text.rb +34 -0
- data/lib/samagotchi/bridge.rb +43 -32
- data/lib/samagotchi/bridge_client.rb +10 -9
- data/lib/samagotchi/broadcast/recipients.rb +2 -2
- data/lib/samagotchi/broadcast/triage.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +5 -5
- data/lib/samagotchi/bundle_command/try/command.rb +1 -1
- data/lib/samagotchi/bundle_command/try/run.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
- data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/child_move.rb +3 -3
- data/lib/samagotchi/commands/registry.rb +6 -6
- data/lib/samagotchi/composer_route.rb +4 -5
- data/lib/samagotchi/config.rb +14 -12
- data/lib/samagotchi/context_candidate_choice.rb +1 -1
- data/lib/samagotchi/context_candidates.rb +4 -3
- data/lib/samagotchi/continue_offer.rb +1 -1
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/degrade.rb +75 -0
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
- data/lib/samagotchi/engine/cards.rb +87 -0
- data/lib/samagotchi/engine/context_judge.rb +138 -0
- data/lib/samagotchi/engine/events.rb +131 -0
- data/lib/samagotchi/engine/guardrails.rb +87 -0
- data/lib/samagotchi/engine/hooks.rb +119 -0
- data/lib/samagotchi/engine/messages.rb +124 -0
- data/lib/samagotchi/engine/model_settings.rb +127 -0
- data/lib/samagotchi/engine/models.rb +261 -0
- data/lib/samagotchi/engine/objectives.rb +116 -0
- data/lib/samagotchi/engine/participants.rb +86 -0
- data/lib/samagotchi/engine/plugins.rb +208 -0
- data/lib/samagotchi/engine/questions.rb +98 -0
- data/lib/samagotchi/engine/recap.rb +130 -0
- data/lib/samagotchi/engine/session.rb +131 -0
- data/lib/samagotchi/engine/side_requests.rb +135 -0
- data/lib/samagotchi/engine/steering.rb +134 -0
- data/lib/samagotchi/engine/toolkit.rb +269 -0
- data/lib/samagotchi/engine/turn_end.rb +180 -0
- data/lib/samagotchi/engine/turn_helpers.rb +160 -0
- data/lib/samagotchi/engine/verify.rb +153 -0
- data/lib/samagotchi/engine.rb +62 -2646
- data/lib/samagotchi/grants.rb +575 -0
- data/lib/samagotchi/guardrail_wiring.rb +2 -2
- data/lib/samagotchi/guardrails/context.rb +2 -3
- data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
- data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
- data/lib/samagotchi/hook_routes.rb +3 -3
- data/lib/samagotchi/idle_recap.rb +4 -5
- data/lib/samagotchi/idle_reminders.rb +3 -4
- data/lib/samagotchi/iteration_limit.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +10 -11
- data/lib/samagotchi/launch_mode.rb +13 -32
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/llm_context_command.rb +3 -2
- data/lib/samagotchi/log.rb +7 -7
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/log_path.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +18 -8
- data/lib/samagotchi/objectives.rb +24 -3
- data/lib/samagotchi/objectives_check.rb +8 -27
- data/lib/samagotchi/objectives_command.rb +4 -69
- data/lib/samagotchi/objectives_edit.rb +87 -0
- data/lib/samagotchi/output_formatter.rb +7 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/parent_answers_command.rb +5 -4
- data/lib/samagotchi/participant_exposure.rb +42 -0
- data/lib/samagotchi/participant_mode.rb +1 -1
- data/lib/samagotchi/participant_name.rb +1 -1
- data/lib/samagotchi/participants.rb +17 -17
- data/lib/samagotchi/participants_watch.rb +1 -1
- data/lib/samagotchi/pending_input_queue.rb +2 -2
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/plugin/context.rb +21 -7
- data/lib/samagotchi/plugin/peer.rb +2 -1
- data/lib/samagotchi/plugin/sessions.rb +6 -4
- data/lib/samagotchi/plugin_tasks.rb +1 -2
- data/lib/samagotchi/private_files.rb +29 -0
- data/lib/samagotchi/project_env.rb +2 -2
- data/lib/samagotchi/question_desk.rb +8 -7
- data/lib/samagotchi/recap_command.rb +2 -1
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/recap_store.rb +1 -1
- data/lib/samagotchi/redact.rb +86 -0
- data/lib/samagotchi/reminder_queue.rb +2 -2
- data/lib/samagotchi/remote_attach_command.rb +1 -1
- data/lib/samagotchi/reply_wait/poll.rb +2 -2
- data/lib/samagotchi/review_start.rb +1 -1
- data/lib/samagotchi/run_options.rb +6 -6
- data/lib/samagotchi/self_report.rb +42 -39
- data/lib/samagotchi/send_command/busy.rb +1 -1
- data/lib/samagotchi/send_command.rb +4 -1
- data/lib/samagotchi/session.rb +11 -2
- data/lib/samagotchi/session_access/contract.rb +3 -3
- data/lib/samagotchi/session_access/local.rb +9 -9
- data/lib/samagotchi/session_access/remote.rb +17 -7
- data/lib/samagotchi/session_access.rb +4 -4
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +8 -6
- data/lib/samagotchi/session_commands.rb +45 -48
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_delete_command.rb +4 -4
- data/lib/samagotchi/session_manager.rb +59 -36
- data/lib/samagotchi/session_metrics.rb +2 -2
- data/lib/samagotchi/session_model_settings.rb +3 -2
- data/lib/samagotchi/session_participants.rb +1 -1
- data/lib/samagotchi/session_retention.rb +3 -3
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +10 -10
- data/lib/samagotchi/steer.rb +1 -1
- data/lib/samagotchi/system_prompt.rb +102 -109
- data/lib/samagotchi/task_watch.rb +1 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
- data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
- data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
- data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
- data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
- data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
- data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
- data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
- data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
- data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
- data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
- data/lib/samagotchi/terminal_ui.rb +142 -1387
- data/lib/samagotchi/title_command.rb +2 -2
- data/lib/samagotchi/tool_runner.rb +14 -16
- data/lib/samagotchi/toolkit_command.rb +2 -1
- data/lib/samagotchi/tools/delegate.rb +3 -1
- data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
- data/lib/samagotchi/tools/delegate_wait.rb +1 -1
- data/lib/samagotchi/tools/objectives_tool.rb +2 -2
- data/lib/samagotchi/tools/send_note.rb +2 -1
- data/lib/samagotchi/turn_endings.rb +2 -2
- data/lib/samagotchi/turn_flow.rb +4 -5
- data/lib/samagotchi/update_hint.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +39 -50
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_review.rb +5 -5
- data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
- data/lib/samagotchi/web/participant_view.rb +2 -82
- data/lib/samagotchi/web/participants_admin.rb +1 -1
- data/lib/samagotchi/web/public/block_host.js +35 -8
- data/lib/samagotchi/web/public/block_renderers.css +9 -5
- data/lib/samagotchi/web/public/chains.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer_mode.js +1 -1
- data/lib/samagotchi/web/public/format.js +3 -3
- data/lib/samagotchi/web/push/store.rb +1 -1
- data/lib/samagotchi/web/session_access_routes.rb +5 -4
- data/lib/samagotchi/web/session_control.rb +12 -8
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +2 -2
- data/lib/samagotchi/web/session_show.rb +2 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +100 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- metadata +29 -6
- data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
- data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
- data/lib/samagotchi/web/grants.rb +0 -575
- data/lib/samagotchi/web/participant_exposure.rb +0 -44
- data/lib/samagotchi/web/private_files.rb +0 -31
|
@@ -8,12 +8,12 @@ require_relative "cli/flags"
|
|
|
8
8
|
require_relative "session"
|
|
9
9
|
require_relative "session_access"
|
|
10
10
|
require_relative "session_access/local"
|
|
11
|
-
require_relative "
|
|
11
|
+
require_relative "grants"
|
|
12
12
|
require_relative "web/qr"
|
|
13
13
|
|
|
14
14
|
module Samagotchi
|
|
15
15
|
# `chi share`: invite a participant to review a session (the
|
|
16
|
-
# shared-sessions plan: a
|
|
16
|
+
# shared-sessions plan: a Grants grant, one session, one person or
|
|
17
17
|
# agent, 7 days unless --expires says otherwise), list the participants,
|
|
18
18
|
# revoke them. Local or through chi --remote NAME (SessionAccess). The
|
|
19
19
|
# token is printed once, in the link; chi keeps only its digest. See
|
|
@@ -95,9 +95,9 @@ module Samagotchi
|
|
|
95
95
|
return parsed if parsed.is_a?(Integer)
|
|
96
96
|
|
|
97
97
|
send(:"run_#{@sub || "invite"}", parsed.options, parsed.args)
|
|
98
|
-
rescue SessionAccess::BadRequest,
|
|
98
|
+
rescue SessionAccess::BadRequest, Grants::BadIdentity => e
|
|
99
99
|
usage_error(e.message)
|
|
100
|
-
rescue
|
|
100
|
+
rescue Grants::Unwritable => e
|
|
101
101
|
error_line("#{command_name}: #{e.message}")
|
|
102
102
|
CLI::Exit::FAILED
|
|
103
103
|
rescue ArgumentError => e
|
|
@@ -114,13 +114,13 @@ module Samagotchi
|
|
|
114
114
|
return usage_error("give one session ID") unless args.size == 1
|
|
115
115
|
return usage_error("--name NAME: who the link is for") if options[:name].to_s.strip.empty?
|
|
116
116
|
|
|
117
|
-
ttl =
|
|
117
|
+
ttl = Grants.parse_ttl(options[:expires])
|
|
118
118
|
return usage_error("--expires takes Nh or Nd, 1h to 30d (e.g. 3d)") if ttl == false
|
|
119
119
|
|
|
120
120
|
cap = options[:cap] && dollars(options[:cap])
|
|
121
121
|
return usage_error(CAP_USAGE) if cap == false
|
|
122
122
|
|
|
123
|
-
identity = options[:identity] &&
|
|
123
|
+
identity = options[:identity] && Grants.clean_identity(options[:identity])
|
|
124
124
|
|
|
125
125
|
id = @access.resolve(args.first)
|
|
126
126
|
invitation = @access.invite(id, name: options[:name], kind: options[:agent] ? "agent" : "human", ttl: ttl, cap: cap,
|
|
@@ -149,13 +149,13 @@ module Samagotchi
|
|
|
149
149
|
0
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
-
CAP_USAGE = "a cap is dollars, $0 to $#{
|
|
152
|
+
CAP_USAGE = "a cap is dollars, $0 to $#{Grants::MAX_CAP.to_i} (e.g. --cap 5)".freeze
|
|
153
153
|
|
|
154
154
|
def run_cap(_options, args)
|
|
155
155
|
return usage_error("give a session ID, a participant and dollars (chi share cap ID p_… 5)") unless args.size == 3
|
|
156
156
|
|
|
157
157
|
participant = args[1]
|
|
158
|
-
return usage_error("not a participant: #{participant} (chi share ls lists them)") unless
|
|
158
|
+
return usage_error("not a participant: #{participant} (chi share ls lists them)") unless Grants::ID_RE.match?(participant)
|
|
159
159
|
|
|
160
160
|
cap = dollars(args[2])
|
|
161
161
|
return usage_error(CAP_USAGE) if cap == false
|
|
@@ -172,7 +172,7 @@ module Samagotchi
|
|
|
172
172
|
return false unless text.match?(/\A\d+(?:\.\d+)?\z/)
|
|
173
173
|
|
|
174
174
|
value = text.to_f
|
|
175
|
-
|
|
175
|
+
Grants.cap?(value) ? value : false
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
def money(value) = value.nil? ? "-" : format("$%.2f", value)
|
|
@@ -184,7 +184,7 @@ module Samagotchi
|
|
|
184
184
|
return usage_error("give a session ID (and a participant), or --all")
|
|
185
185
|
end
|
|
186
186
|
participant = args[1]
|
|
187
|
-
return usage_error("not a participant: #{participant} (chi share ls lists them)") if participant && !
|
|
187
|
+
return usage_error("not a participant: #{participant} (chi share ls lists them)") if participant && !Grants::ID_RE.match?(participant)
|
|
188
188
|
|
|
189
189
|
id = args.first && @access.resolve(args.first)
|
|
190
190
|
revoked = @access.revoke_participants(id, participant: participant)
|
data/lib/samagotchi/steer.rb
CHANGED
|
@@ -136,7 +136,7 @@ module Samagotchi
|
|
|
136
136
|
PARTICIPANT_SOURCE = "participant"
|
|
137
137
|
# The owner's author, when one is written out; absent means the same.
|
|
138
138
|
OWNER_AUTHOR = "owner"
|
|
139
|
-
# A participant's author: this prefix, then its id (
|
|
139
|
+
# A participant's author: this prefix, then its id (Grants'
|
|
140
140
|
# grant id), "p:<id>" (#author_for, #participant_id).
|
|
141
141
|
PARTICIPANT_PREFIX = "p:"
|
|
142
142
|
# A participant's message's keys, which every message copy keeps.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "config"
|
|
4
4
|
require_relative "log"
|
|
5
|
+
require_relative "degrade"
|
|
5
6
|
require_relative "log_path"
|
|
6
7
|
require_relative "memory_paths"
|
|
7
8
|
require_relative "paths"
|
|
@@ -74,11 +75,7 @@ module Samagotchi
|
|
|
74
75
|
@requested_memories = effective_preload_list(preload_memory_list(memories))
|
|
75
76
|
# Read once, as the memories: list is: a config edit is picked up by
|
|
76
77
|
# a new process. The files' text is read at each build.
|
|
77
|
-
@config_appended =
|
|
78
|
-
ConfigFile.appended_prompt_files
|
|
79
|
-
rescue StandardError
|
|
80
|
-
[]
|
|
81
|
-
end
|
|
78
|
+
@config_appended = Degrade.call(:prompt, "appended_list_unreadable", fallback: []) { ConfigFile.appended_prompt_files }
|
|
82
79
|
@cli_appended = Array(appended_files).map { |path| File.expand_path(path.to_s) }
|
|
83
80
|
end
|
|
84
81
|
|
|
@@ -386,11 +383,11 @@ module Samagotchi
|
|
|
386
383
|
# (Plugin::PromptSections). nil with none: the prompt is what it was
|
|
387
384
|
# before prompt sections.
|
|
388
385
|
def plugin_prompt_section
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
386
|
+
Degrade.call(:prompt, "plugin_section_failed") do
|
|
387
|
+
built = Array(@sections_lookup.call)
|
|
388
|
+
@section_plugins = built
|
|
389
|
+
Plugin::PromptSections.text(built)
|
|
390
|
+
end
|
|
394
391
|
end
|
|
395
392
|
|
|
396
393
|
def with_tail_declarations(prompt, chat:, layers:)
|
|
@@ -405,35 +402,35 @@ module Samagotchi
|
|
|
405
402
|
# Identity is not tracked as an "activated" memory for the sticky status line
|
|
406
403
|
# to avoid always showing `mem: identity`.
|
|
407
404
|
def system_identity_section
|
|
408
|
-
|
|
409
|
-
|
|
405
|
+
Degrade.call(:prompt, "identity_unreadable") do
|
|
406
|
+
DEFAULT_SYSTEM_MEMORIES.each do |name|
|
|
407
|
+
next if memory_muted?(name)
|
|
410
408
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
409
|
+
body = Tools::MemoryRead.call(name, scope: "system", **overlay_keys)
|
|
410
|
+
next if body.start_with?("Error:")
|
|
411
|
+
next if body.strip.empty?
|
|
414
412
|
|
|
415
|
-
|
|
413
|
+
return "System identity (auto-loaded, scope=system):\n#{body}"
|
|
414
|
+
end
|
|
415
|
+
nil
|
|
416
416
|
end
|
|
417
|
-
nil
|
|
418
|
-
rescue StandardError
|
|
419
|
-
nil
|
|
420
417
|
end
|
|
421
418
|
|
|
422
419
|
# The model notes for the session's model (ModelNotes), right after
|
|
423
420
|
# identity: a heading per scope, each note under its name, the models:
|
|
424
421
|
# line left out. nil without any, so the prompt stays as it was.
|
|
425
422
|
def model_notes_section
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
423
|
+
Degrade.call(:prompt, "model_notes_failed") do
|
|
424
|
+
@section_notes = []
|
|
425
|
+
notes = Array(@model_notes_lookup.call)
|
|
426
|
+
return nil if notes.empty?
|
|
427
|
+
|
|
428
|
+
ref = @model_lookup.call&.ref
|
|
429
|
+
notes.group_by(&:scope).map do |scope, list|
|
|
430
|
+
heading = ref.to_s.empty? ? "Model notes (scope=#{scope}):" : "Model notes (for #{ref}, scope=#{scope}):"
|
|
431
|
+
[heading, *list.map { |note| "## #{note.name}\n#{note.body}" }].join("\n\n")
|
|
432
|
+
end.join("\n\n").tap { @section_notes = notes }
|
|
433
|
+
end
|
|
437
434
|
end
|
|
438
435
|
|
|
439
436
|
# ── Memory helpers ─────────────────────────────────────────────────────────
|
|
@@ -441,23 +438,23 @@ module Samagotchi
|
|
|
441
438
|
# The session model's overlay keys, so the prompt's own memory bodies
|
|
442
439
|
# (identity, preloads) get `<name>.<key>.md` as memory_read gives it.
|
|
443
440
|
def overlay_keys
|
|
444
|
-
|
|
445
|
-
|
|
441
|
+
Degrade.call(:prompt, "overlay_keys_failed", fallback: {}) do
|
|
442
|
+
model = @model_lookup.call
|
|
443
|
+
return {} unless model&.key
|
|
446
444
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
{}
|
|
445
|
+
{ model_key: model.key, fallback_model_key: model.fallback_key }
|
|
446
|
+
end
|
|
450
447
|
end
|
|
451
448
|
|
|
452
449
|
# { system: { tokens:, lines: }, project: { … } } of the index texts
|
|
453
450
|
# the prompt holds; nil when they can't be measured.
|
|
454
451
|
def measure_indexes(indexes)
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
[
|
|
452
|
+
Degrade.call(:prompt, "index_size_failed") do
|
|
453
|
+
chars_per_token = MemoryBundle::IndexSize.chars_per_token
|
|
454
|
+
%i[system project].to_h do |scope|
|
|
455
|
+
[scope, MemoryBundle::IndexSize.of_text(scope, indexes[scope], chars_per_token: chars_per_token).figures]
|
|
456
|
+
end
|
|
458
457
|
end
|
|
459
|
-
rescue StandardError
|
|
460
|
-
nil
|
|
461
458
|
end
|
|
462
459
|
|
|
463
460
|
# The scope's index text without the muted memories' lines, nor the
|
|
@@ -473,11 +470,7 @@ module Samagotchi
|
|
|
473
470
|
# comma-split and appended without duplicates (same ref shape as --memory:
|
|
474
471
|
# bare name or scope/name).
|
|
475
472
|
def preload_memory_list(cli_memories)
|
|
476
|
-
baseline =
|
|
477
|
-
ConfigFile.preloaded_memories
|
|
478
|
-
rescue StandardError
|
|
479
|
-
[]
|
|
480
|
-
end
|
|
473
|
+
baseline = Degrade.call(:prompt, "preload_list_unreadable", fallback: []) { ConfigFile.preloaded_memories }
|
|
481
474
|
|
|
482
475
|
merged = Array(baseline).dup
|
|
483
476
|
# For the warning when one can't be loaded: it names where it came from.
|
|
@@ -555,15 +548,15 @@ module Samagotchi
|
|
|
555
548
|
# The AGENTS.md chain from the top of the git work tree down to the cwd
|
|
556
549
|
# (PromptFiles.agents_sections), one section per file with text.
|
|
557
550
|
def project_specific_description
|
|
558
|
-
|
|
551
|
+
Degrade.call(:prompt, "agents_md_unreadable") do
|
|
552
|
+
return nil if skip_agent_description?
|
|
559
553
|
|
|
560
|
-
|
|
561
|
-
|
|
554
|
+
sections = PromptFiles.agents_sections(Dir.pwd)
|
|
555
|
+
return nil if sections.empty?
|
|
562
556
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
nil
|
|
557
|
+
@section_files&.concat(sections.map(&:path))
|
|
558
|
+
sections.map(&:text).join("\n\n")
|
|
559
|
+
end
|
|
567
560
|
end
|
|
568
561
|
|
|
569
562
|
# The appended files' text, right after AGENTS.md (the stable, cached
|
|
@@ -572,26 +565,26 @@ module Samagotchi
|
|
|
572
565
|
# then --append-system-prompt's. A file the AGENTS.md chain holds, a
|
|
573
566
|
# repeat and an empty or vanished file are left out.
|
|
574
567
|
def appended_section
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
568
|
+
Degrade.call(:prompt, "appended_files_failed") do
|
|
569
|
+
cwd = Dir.pwd
|
|
570
|
+
top = MemoryPaths.work_tree_root(cwd)
|
|
571
|
+
seen = Set.new((skip_agent_description? ? [] : PromptFiles.agents_paths(cwd)).map { |path| canonical_path(path) })
|
|
572
|
+
sections = []
|
|
573
|
+
appended_candidates(cwd).each do |path, source|
|
|
574
|
+
next unless seen.add?(canonical_path(path))
|
|
575
|
+
|
|
576
|
+
unless File.file?(path)
|
|
577
|
+
Log.debug(:config, "appended_prompt_file_missing", path: path, source: source)
|
|
578
|
+
next
|
|
579
|
+
end
|
|
580
|
+
text = PromptFiles.section(path, "Appended from #{PromptFiles.display_path(path, top: top)}:")
|
|
581
|
+
next unless text
|
|
588
582
|
|
|
589
|
-
|
|
590
|
-
|
|
583
|
+
@section_files&.push(path)
|
|
584
|
+
sections << text
|
|
585
|
+
end
|
|
586
|
+
sections.empty? ? nil : sections.join("\n\n")
|
|
591
587
|
end
|
|
592
|
-
sections.empty? ? nil : sections.join("\n\n")
|
|
593
|
-
rescue StandardError
|
|
594
|
-
nil
|
|
595
588
|
end
|
|
596
589
|
|
|
597
590
|
# [absolute path, "config" | "cli"], in prompt order.
|
|
@@ -617,27 +610,27 @@ module Samagotchi
|
|
|
617
610
|
# The home directory is spelled out once so the model copies the right
|
|
618
611
|
# sequence, with the advice to write it as ~ or $HOME instead.
|
|
619
612
|
def project_location
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
613
|
+
Degrade.call(:prompt, "location_failed") do
|
|
614
|
+
cwd = Dir.pwd
|
|
615
|
+
top = MemoryPaths.work_tree_root(cwd)
|
|
616
|
+
lines = ["Current working directory:", cwd]
|
|
617
|
+
if top && top != cwd
|
|
618
|
+
lines << "Top of this checkout (where the project's commands run from; stay inside it):"
|
|
619
|
+
lines << top
|
|
620
|
+
elsif top
|
|
621
|
+
lines << "This checkout (the project's files; its commands run here):"
|
|
622
|
+
lines << top
|
|
623
|
+
end
|
|
624
|
+
if top && MemoryPaths.project_root(cwd) != top
|
|
625
|
+
lines << "This checkout is a linked git worktree: read, edit, run and commit only in it, never in the " \
|
|
626
|
+
"repository's other checkouts."
|
|
627
|
+
end
|
|
628
|
+
home = Dir.home
|
|
629
|
+
lines << "Home directory: #{home} (write it as ~ or $HOME in commands and paths)" unless home.to_s.empty?
|
|
630
|
+
lines << "Project memories folder (your notes about the project, not its files; never work or run commands there):"
|
|
631
|
+
lines << Paths.tilde(Tools::MemoryRead.memories_dir("project"))
|
|
632
|
+
lines.join("\n")
|
|
633
633
|
end
|
|
634
|
-
home = Dir.home
|
|
635
|
-
lines << "Home directory: #{home} (write it as ~ or $HOME in commands and paths)" unless home.to_s.empty?
|
|
636
|
-
lines << "Project memories folder (your notes about the project, not its files; never work or run commands there):"
|
|
637
|
-
lines << Paths.tilde(Tools::MemoryRead.memories_dir("project"))
|
|
638
|
-
lines.join("\n")
|
|
639
|
-
rescue StandardError
|
|
640
|
-
nil
|
|
641
634
|
end
|
|
642
635
|
|
|
643
636
|
# Only while the session has participants (people its owner invited to
|
|
@@ -662,12 +655,12 @@ module Samagotchi
|
|
|
662
655
|
# once a participant invited to act is in a working session, the review
|
|
663
656
|
# line for review participants; nothing with none (nil, false).
|
|
664
657
|
def participants_section
|
|
665
|
-
|
|
666
|
-
|
|
658
|
+
Degrade.call(:prompt, "participants_line_failed") do
|
|
659
|
+
mode = @participants_lookup.call
|
|
660
|
+
return nil unless mode
|
|
667
661
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
nil
|
|
662
|
+
mode == ParticipantMode::PAIR ? PAIR_PARTICIPANTS_LINE : PARTICIPANTS_LINE
|
|
663
|
+
end
|
|
671
664
|
end
|
|
672
665
|
|
|
673
666
|
# The model this session runs on. A model guesses its name from
|
|
@@ -675,19 +668,19 @@ module Samagotchi
|
|
|
675
668
|
# says to answer from here. It depends only on the effective model, and
|
|
676
669
|
# the prompt is rebuilt only on a switch (#reset!): no per-turn churn.
|
|
677
670
|
def current_model
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
"
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
671
|
+
Degrade.call(:prompt, "model_line_failed") do
|
|
672
|
+
model = @model_lookup.call
|
|
673
|
+
return nil unless model && !model.ref.to_s.empty?
|
|
674
|
+
|
|
675
|
+
details = [model.host, model.key && "model key #{model.key}", model.served && "the server says it serves #{model.served}"]
|
|
676
|
+
details = details.compact.reject(&:empty?)
|
|
677
|
+
where = details.empty? ? "" : " (#{details.join("; ")})"
|
|
678
|
+
"Model: this session runs on #{model.ref}#{where}.\n" \
|
|
679
|
+
"Asked which model you are, answer with this line, not from training: a fine-tuned model often knows only " \
|
|
680
|
+
"its base model's name, but this session runs what is named here; it changes only with /model. " \
|
|
681
|
+
"Guidance for this model only goes in a model note: a memory named model_notes_<name> whose first line is " \
|
|
682
|
+
"`models: <glob on this model's id or key>`."
|
|
683
|
+
end
|
|
691
684
|
end
|
|
692
685
|
|
|
693
686
|
# Fixed for the session's lifetime, so it doesn't churn the prompt cache.
|
|
@@ -701,7 +694,7 @@ module Samagotchi
|
|
|
701
694
|
line = "Current session id: #{id} (resume later with `chi --resume #{id}`)"
|
|
702
695
|
# The log path too: asked what went wrong, a model that has to look
|
|
703
696
|
# it up guesses ~/.local/state first (the self-awareness probes).
|
|
704
|
-
log =
|
|
697
|
+
log = Degrade.call(:prompt, "log_path_failed") { LogPath.resolve }
|
|
705
698
|
line = "#{line}\nMy debug log: #{log} (one record per line; this session's carry sid=#{id[0, Log::SID_LENGTH]})" if log
|
|
706
699
|
parent = session.parent_id.to_s
|
|
707
700
|
return line if parent.empty?
|
|
@@ -9,7 +9,7 @@ require_relative "tools/task_runtime"
|
|
|
9
9
|
module Samagotchi
|
|
10
10
|
# The background tasks a worker's session started that the model hasn't
|
|
11
11
|
# seen finish (docs/internals/background-tasks.md, Finish wake). Held by
|
|
12
|
-
# the Worker (
|
|
12
|
+
# the Worker (a --non-interactive one-shot has none) and fed by the Engine's raw
|
|
13
13
|
# :tool_call_completed events (#observe), the input Bridge#own_task_ids
|
|
14
14
|
# reads too; the idle loop takes the finished ones (#take_finished) for a
|
|
15
15
|
# wake turn or a plain note. In memory only: a crash loses them, and they
|
|
@@ -13,14 +13,19 @@ require_relative "../launch_mode"
|
|
|
13
13
|
require_relative "live_region"
|
|
14
14
|
require_relative "plain_surface"
|
|
15
15
|
require_relative "attached_loop"
|
|
16
|
+
require_relative "log_tail"
|
|
17
|
+
require_relative "formatting"
|
|
16
18
|
|
|
17
19
|
module Samagotchi
|
|
18
20
|
class TerminalUI
|
|
19
|
-
# `chi --attach ID` and `chi --
|
|
21
|
+
# `chi`, `chi --attach ID` and `chi --resume ID`: find or start the
|
|
20
22
|
# session's worker, then run the TUI as a client of its Bridge
|
|
21
23
|
# (AttachedLoop). It takes no OwnerLock and builds no Engine: the worker
|
|
22
24
|
# owns the session, and any number of UIs can attach to it.
|
|
23
25
|
module AttachLauncher
|
|
26
|
+
# #paint, for the -v records.
|
|
27
|
+
extend Formatting
|
|
28
|
+
|
|
24
29
|
# Why attaching failed, worded for the terminal.
|
|
25
30
|
class Error < StandardError; end
|
|
26
31
|
|
|
@@ -28,6 +33,13 @@ module Samagotchi
|
|
|
28
33
|
# A connector that answered no session (its own failures raise Error
|
|
29
34
|
# with their words).
|
|
30
35
|
NOTHING_CONNECTED = "could not attach: no session was found or started"
|
|
36
|
+
# `chi scratch`'s first line, and its last once the session is gone.
|
|
37
|
+
SCRATCH_BANNER = "Scratch session: nothing is kept, it is deleted when you leave."
|
|
38
|
+
SCRATCH_DELETED = "Scratch session deleted."
|
|
39
|
+
# chi -v on a worker that was running before this launch (%s: the
|
|
40
|
+
# debug log): its level is its own.
|
|
41
|
+
VERBOSE_RUNNING = "(--verbose: this worker was already running and logs at the level it started with; " \
|
|
42
|
+
"its records show here only if it writes to %s: one chi web started may log elsewhere)"
|
|
31
43
|
|
|
32
44
|
module_function
|
|
33
45
|
|
|
@@ -62,6 +74,13 @@ module Samagotchi
|
|
|
62
74
|
# own guardrails.parent_answers; an existing session keeps its own
|
|
63
75
|
# (never sent as a /parent-answers command: that would be a
|
|
64
76
|
# loosening route from a command line)
|
|
77
|
+
# @param scratch [Boolean] `chi scratch`: a new scratch session (its
|
|
78
|
+
# worker's Engine saves nothing), stopped and deleted however the
|
|
79
|
+
# attach ends; its worker deletes it too once no client is left
|
|
80
|
+
# @param verbose [Boolean] -v: the session's debug records in the
|
|
81
|
+
# debug log (Log.path), shown above the live region as they come
|
|
82
|
+
# (LogTail); a worker this launch spawns logs at debug (bin/chi's
|
|
83
|
+
# log.level override reaches it through Config.cli_env)
|
|
65
84
|
# @param state_dir [String, nil] where the sessions are (the default
|
|
66
85
|
# state dir by default): the worker's, for the attached loop too
|
|
67
86
|
# @param connector [#connect, nil] finds or starts the session
|
|
@@ -73,19 +92,23 @@ module Samagotchi
|
|
|
73
92
|
# :failed when the --model switch didn't go through, or (input from a
|
|
74
93
|
# pipe) :turn_failed / :empty_answer / :removed / :unanswered
|
|
75
94
|
# (AttachedLoop#run)
|
|
76
|
-
def run(attach: nil,
|
|
95
|
+
def run(attach: nil, resume: nil, prompt: nil, model: nil, no_interrupt: false, default_input: true,
|
|
77
96
|
memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], toolkit: nil,
|
|
78
|
-
toolkit_flags: [], settings: nil, parent_answers: nil,
|
|
97
|
+
toolkit_flags: [], settings: nil, parent_answers: nil, scratch: false, verbose: false, state_dir: nil,
|
|
98
|
+
connector: nil)
|
|
79
99
|
state_dir ||= Session.default_state_dir
|
|
100
|
+
running_before = verbose && !connector && worker_running?(attach || resume, state_dir)
|
|
80
101
|
link = connector && (connector.connect || raise(Error, NOTHING_CONNECTED))
|
|
81
102
|
client = if connector
|
|
82
103
|
link.client
|
|
83
104
|
else
|
|
84
|
-
connect(attach: attach,
|
|
105
|
+
connect(attach: attach, resume: resume, model: model, state_dir: state_dir,
|
|
85
106
|
memories: memories, muted_memories: muted_memories, llm_context: llm_context,
|
|
86
107
|
thinking: thinking, appended_prompt_files: appended_prompt_files, toolkit: toolkit,
|
|
87
|
-
toolkit_flags: toolkit_flags, settings: settings, parent_answers: parent_answers
|
|
108
|
+
toolkit_flags: toolkit_flags, settings: settings, parent_answers: parent_answers,
|
|
109
|
+
scratch: scratch)
|
|
88
110
|
end
|
|
111
|
+
$stdout.puts(SCRATCH_BANNER) if scratch
|
|
89
112
|
commands = []
|
|
90
113
|
# --yes on /model and /thinking: typing the flag is the choice (no
|
|
91
114
|
# confirm step); its applied reply ends the launch's command.
|
|
@@ -93,11 +116,13 @@ module Samagotchi
|
|
|
93
116
|
commands << LLMContextOverride.command_line(llm_context) if (attach || resume) && !llm_context.empty?
|
|
94
117
|
commands << "#{ThinkingCommand::NAME} #{thinking} --yes" if (attach || resume) && thinking
|
|
95
118
|
first_command = commands.size > 1 ? commands : commands.first
|
|
119
|
+
$stderr.puts(format(VERBOSE_RUNNING, Log.path)) if running_before
|
|
96
120
|
surface = open_surface
|
|
121
|
+
tail = (start_log_tail(surface, client.session_id) if verbose && Log.path)
|
|
97
122
|
options = { screen: surface, client_id: client_id(connector), first_prompt: prompt,
|
|
98
123
|
first_command: first_command, no_interrupt: no_interrupt,
|
|
99
124
|
default_input: default_input && !prompt && !attach && !resume,
|
|
100
|
-
wait_at_eof: !$stdin.tty?,
|
|
125
|
+
wait_at_eof: !$stdin.tty?, scratch: scratch,
|
|
101
126
|
as_parent: Guardrails::ParentApprovals.parent_process? }
|
|
102
127
|
attached = if connector
|
|
103
128
|
AttachedLoop.new(link: link, **options)
|
|
@@ -107,6 +132,7 @@ module Samagotchi
|
|
|
107
132
|
ended = begin
|
|
108
133
|
attached.run
|
|
109
134
|
ensure
|
|
135
|
+
tail&.stop
|
|
110
136
|
close_surface(surface)
|
|
111
137
|
end
|
|
112
138
|
if ended == :unanswered && attached.unanswered
|
|
@@ -119,6 +145,34 @@ module Samagotchi
|
|
|
119
145
|
ended
|
|
120
146
|
ensure
|
|
121
147
|
link&.close
|
|
148
|
+
# Every way out (Ctrl-D, /exit, the worker gone, an error, Ctrl-C or
|
|
149
|
+
# a signal): only kill -9 leaves it to its worker (and the sweep).
|
|
150
|
+
end_scratch(client.session_id, state_dir: state_dir, quiet: !$!.nil?) if scratch && client
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Whether +session_id+'s worker runs now (nil: a new session).
|
|
154
|
+
def worker_running?(session_id, state_dir)
|
|
155
|
+
return false unless session_id && Session.valid_id?(session_id)
|
|
156
|
+
|
|
157
|
+
WorkerLink.resolve(Session.session_dir(session_id, state_dir: state_dir), unlink: false).live?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# The session's debug records, dim, above the live region.
|
|
161
|
+
def start_log_tail(surface, session_id)
|
|
162
|
+
LogTail.new(path: Log.path, session_id: session_id, on_record: lambda { |record|
|
|
163
|
+
surface.commit(paint(LogLine.format(record).chomp, 2))
|
|
164
|
+
}).start
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Stop the scratch session's worker and delete the session; its worker
|
|
168
|
+
# may have deleted it first (it found no client left).
|
|
169
|
+
def end_scratch(session_id, state_dir:, quiet: false, out: $stdout, err: $stderr)
|
|
170
|
+
SessionManager.delete_session(session_id, state_dir: state_dir, stop: true, wait: LocalLink::DELETE_WAIT)
|
|
171
|
+
out.puts(SCRATCH_DELETED) unless quiet
|
|
172
|
+
rescue ArgumentError
|
|
173
|
+
out.puts(SCRATCH_DELETED) unless quiet
|
|
174
|
+
rescue SessionManager::DeleteRefused, SystemCallError => e
|
|
175
|
+
err.puts("Scratch session #{session_id} was not deleted (#{e.message}): chi sessions delete #{session_id}")
|
|
122
176
|
end
|
|
123
177
|
|
|
124
178
|
# This TUI's client id: tui:PID, or the connector's own (a remote one
|
|
@@ -185,10 +239,11 @@ module Samagotchi
|
|
|
185
239
|
|
|
186
240
|
# @return [BridgeClient] a client for the live Bridge of the session
|
|
187
241
|
# @raise [Error]
|
|
188
|
-
def connect(attach: nil,
|
|
242
|
+
def connect(attach: nil, resume: nil, model: nil, state_dir: nil, wait: BRIDGE_WAIT,
|
|
189
243
|
memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], toolkit: nil,
|
|
190
|
-
toolkit_flags: [], settings: nil, parent_answers: nil, err: $stderr)
|
|
244
|
+
toolkit_flags: [], settings: nil, parent_answers: nil, scratch: false, err: $stderr)
|
|
191
245
|
sd = state_dir || Session.default_state_dir
|
|
246
|
+
connected = false
|
|
192
247
|
if attach || resume
|
|
193
248
|
warn_memory_flags_ignored(attach || resume, memories, muted_memories, err, appended_prompt_files: appended_prompt_files,
|
|
194
249
|
toolkit_flags: toolkit_flags)
|
|
@@ -211,14 +266,21 @@ module Samagotchi
|
|
|
211
266
|
thinking: Thinking.session_level(thinking),
|
|
212
267
|
appended_prompt_files: appended_prompt_files,
|
|
213
268
|
toolkit: toolkit, settings: settings,
|
|
214
|
-
parent_answers: parent_answers)
|
|
269
|
+
parent_answers: parent_answers),
|
|
270
|
+
scratch: scratch)
|
|
215
271
|
end
|
|
216
272
|
link = WorkerLink.await(session.id, session_dir: Session.session_dir(session.id, state_dir: sd), timeout: wait)
|
|
217
|
-
|
|
273
|
+
raise(Error, format(LaunchMode::BRIDGE_LATE, session.id)) unless link.live?
|
|
274
|
+
|
|
275
|
+
connected = true
|
|
276
|
+
link.client(session.id)
|
|
218
277
|
rescue SessionManager::OwnedByTUI => e
|
|
219
278
|
raise Error, e.message
|
|
220
279
|
rescue ArgumentError => e
|
|
221
280
|
raise Error, e.message
|
|
281
|
+
ensure
|
|
282
|
+
# A scratch session whose Bridge never came (or Ctrl-C meanwhile).
|
|
283
|
+
end_scratch(session.id, state_dir: sd, quiet: true, err: err) if scratch && session && !connected
|
|
222
284
|
end
|
|
223
285
|
|
|
224
286
|
# The session's previous worker may still be leaving: its Bridge
|