samagotchi 0.58.0 → 0.60.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 +86 -1
- data/README.md +3 -2
- data/bin/chi +23 -2
- data/docs/architecture.md +6 -4
- data/docs/cli.md +14 -7
- data/docs/configuration.md +96 -31
- data/docs/hooks.md +12 -6
- data/docs/internals/background-tasks.md +7 -0
- data/docs/internals/engine.md +20 -12
- data/docs/internals/gemma4-contract.md +1 -0
- data/docs/internals/prompt-caching.md +4 -3
- data/docs/internals/tool-guardrails.md +2 -1
- data/docs/memory.md +1 -1
- data/docs/plugins.md +75 -6
- data/docs/sessions.md +2 -2
- data/docs/sub-agent.md +2 -2
- data/docs/testing.md +4 -2
- data/lib/samagotchi/bootstrap_command.rb +2 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -0
- data/lib/samagotchi/bridge.rb +6 -2
- data/lib/samagotchi/bundles/agents-md/manifest.yml +11 -0
- data/lib/samagotchi/bundles/agents-md/plugin.rb +198 -0
- data/lib/samagotchi/bundles/core/manifest.yml +2 -1
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +5 -4
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +169 -16
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/config.rb +44 -10
- data/lib/samagotchi/continue_offer.rb +5 -2
- data/lib/samagotchi/engine.rb +89 -15
- data/lib/samagotchi/host_config.rb +3 -1
- data/lib/samagotchi/host_registry.rb +2 -1
- data/lib/samagotchi/iteration_limit.rb +3 -2
- data/lib/samagotchi/kernel_loop.rb +35 -9
- data/lib/samagotchi/llm/backend.rb +6 -1
- data/lib/samagotchi/llm/chat_loop.rb +11 -6
- data/lib/samagotchi/llm/model_result.rb +11 -2
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/model_settings.rb +4 -2
- data/lib/samagotchi/plugin/context.rb +16 -3
- data/lib/samagotchi/process_group.rb +76 -3
- data/lib/samagotchi/prompt_files.rb +127 -0
- data/lib/samagotchi/ref.rb +124 -0
- data/lib/samagotchi/send_command.rb +1 -1
- data/lib/samagotchi/session.rb +15 -3
- data/lib/samagotchi/session_commands.rb +15 -3
- data/lib/samagotchi/session_metrics.rb +8 -2
- data/lib/samagotchi/session_setup.rb +11 -5
- data/lib/samagotchi/steer_cut.rb +12 -8
- data/lib/samagotchi/steer_cut_after.rb +69 -0
- data/lib/samagotchi/system_prompt.rb +74 -18
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +18 -9
- data/lib/samagotchi/terminal_ui/attached_loop.rb +1 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +23 -5
- data/lib/samagotchi/terminal_ui.rb +18 -6
- data/lib/samagotchi/tool_activity.rb +18 -1
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tools/execute.rb +318 -70
- data/lib/samagotchi/tools/task_get.rb +2 -0
- data/lib/samagotchi/tools/task_runtime.rb +96 -13
- data/lib/samagotchi/tools/task_wait.rb +23 -5
- data/lib/samagotchi/turn_flow.rb +22 -3
- data/lib/samagotchi/utf8_default.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/favicon.svg +1 -0
- data/lib/samagotchi/web/public/index.html +1 -0
- data/lib/samagotchi/web/public/read_view.js +3 -1
- data/lib/samagotchi/web/public/refs.js +107 -15
- data/lib/samagotchi/web/public/task_stop.js +25 -7
- data/lib/samagotchi/worker.rb +1 -0
- metadata +7 -1
|
@@ -11,12 +11,13 @@ require_relative "model_notes"
|
|
|
11
11
|
require_relative "bundle_needs"
|
|
12
12
|
require_relative "memory_bundle/index_size"
|
|
13
13
|
require_relative "thinking"
|
|
14
|
+
require_relative "prompt_files"
|
|
14
15
|
|
|
15
16
|
module Samagotchi
|
|
16
17
|
# The system prompt an Engine gives its model: the base prompt (tool
|
|
17
18
|
# declarations and call syntax for the raw-prompt loop, none for the chat
|
|
18
19
|
# loop, the shared guidance) and around it the thinking token, rg
|
|
19
|
-
# guidance, the identity memory, the model notes, the preloaded memories,
|
|
20
|
+
# guidance, the identity memory, the model notes, the preloaded memories, AGENTS.md, the
|
|
20
21
|
# memory indexes and, last, the model, where the session runs and the
|
|
21
22
|
# session (#system_prompt_with_index has the order and why).
|
|
22
23
|
#
|
|
@@ -24,8 +25,6 @@ module Samagotchi
|
|
|
24
25
|
# tools a plugin replaces, the attached session, the thinking level) is
|
|
25
26
|
# read at build time through the lookups; the memory lists are fixed.
|
|
26
27
|
class SystemPrompt
|
|
27
|
-
AGENT_DESCRIPTION_FILE = "AGENT.md"
|
|
28
|
-
|
|
29
28
|
# The model a session runs on, as the prompt names it: the resolved
|
|
30
29
|
# ref, its host ("name, host:port" or url), the memory overlay key, and
|
|
31
30
|
# what a llama.cpp server says it serves when that differs (else nil).
|
|
@@ -51,8 +50,10 @@ module Samagotchi
|
|
|
51
50
|
# model's (none: the section is left out)
|
|
52
51
|
# @param memories [Array<String>] the --memory list
|
|
53
52
|
# @param muted_memory_names [Array<String>] normalized (MutedMemories)
|
|
53
|
+
# @param appended_files [Array<String>] chi --append-system-prompt's
|
|
54
|
+
# files (absolute paths), after config.yml's append_system_prompt:
|
|
54
55
|
def initialize(profile:, tools:, session:, thinking:, model: -> {}, model_notes: -> { [] }, memories: [],
|
|
55
|
-
muted_memory_names: [])
|
|
56
|
+
muted_memory_names: [], appended_files: [])
|
|
56
57
|
@profile_lookup = profile
|
|
57
58
|
@model_lookup = model
|
|
58
59
|
@model_notes_lookup = model_notes
|
|
@@ -61,6 +62,14 @@ module Samagotchi
|
|
|
61
62
|
@thinking_lookup = thinking
|
|
62
63
|
@muted_memory_names = muted_memory_names
|
|
63
64
|
@requested_memories = effective_preload_list(preload_memory_list(memories))
|
|
65
|
+
# Read once, as the memories: list is: a config edit is picked up by
|
|
66
|
+
# a new process. The files' text is read at each build.
|
|
67
|
+
@config_appended = begin
|
|
68
|
+
ConfigFile.appended_prompt_files
|
|
69
|
+
rescue StandardError
|
|
70
|
+
[]
|
|
71
|
+
end
|
|
72
|
+
@cli_appended = Array(appended_files).map { |path| File.expand_path(path.to_s) }
|
|
64
73
|
end
|
|
65
74
|
|
|
66
75
|
# @return [Array<String>] the names the session preloads, known before
|
|
@@ -81,16 +90,20 @@ module Samagotchi
|
|
|
81
90
|
@built ||= {}
|
|
82
91
|
@index_sizes ||= {}
|
|
83
92
|
@notes_loaded ||= {}
|
|
93
|
+
@files_loaded ||= {}
|
|
84
94
|
unless @built.key?(key)
|
|
85
95
|
@section_notes = nil
|
|
96
|
+
@section_files = []
|
|
86
97
|
@built[key] = system_prompt_with_index(
|
|
87
98
|
assist_system_prompt(chat: chat, thinking: thinking, layers: layers), chat: chat, thinking: thinking, layers: layers
|
|
88
99
|
)
|
|
89
100
|
@index_sizes[key] = @measured_index
|
|
90
101
|
@notes_loaded[key] = @section_notes || []
|
|
102
|
+
@files_loaded[key] = @section_files.freeze
|
|
91
103
|
end
|
|
92
104
|
@memory_index = @index_sizes[key]
|
|
93
105
|
@prompt_notes = @notes_loaded[key]
|
|
106
|
+
@prompt_files = @files_loaded[key]
|
|
94
107
|
@built[key]
|
|
95
108
|
end
|
|
96
109
|
|
|
@@ -108,6 +121,14 @@ module Samagotchi
|
|
|
108
121
|
# @return [Array<ModelNotes::Note>, nil]
|
|
109
122
|
attr_reader :prompt_notes
|
|
110
123
|
|
|
124
|
+
# The files whose text the prompt #build returned last holds, as
|
|
125
|
+
# absolute paths in prompt order: the AGENTS.md chain, then the
|
|
126
|
+
# appended files (append_system_prompt:, --append-system-prompt). [] before a
|
|
127
|
+
# build and after #reset!. Read when the prompt is built, as the
|
|
128
|
+
# sections are.
|
|
129
|
+
# @return [Array<String>]
|
|
130
|
+
def prompt_files = @prompt_files || []
|
|
131
|
+
|
|
111
132
|
# Drops the built prompts: the next #build reads the profile, tools,
|
|
112
133
|
# indexes and memories again (a model or profile switch, changed tools).
|
|
113
134
|
def reset!
|
|
@@ -115,6 +136,8 @@ module Samagotchi
|
|
|
115
136
|
@index_sizes = nil
|
|
116
137
|
@notes_loaded = nil
|
|
117
138
|
@prompt_notes = nil
|
|
139
|
+
@files_loaded = nil
|
|
140
|
+
@prompt_files = nil
|
|
118
141
|
@stable_lengths = nil
|
|
119
142
|
end
|
|
120
143
|
|
|
@@ -282,7 +305,7 @@ module Samagotchi
|
|
|
282
305
|
# Ordered for the servers' prompt caches (they reuse only an exact token
|
|
283
306
|
# prefix): what every session shares first, what changes least before
|
|
284
307
|
# what changes more, and the per-session lines last. The global parts
|
|
285
|
-
# (identity, preloads) come before the project's
|
|
308
|
+
# (identity, preloads) come before the project's AGENTS.md, the memory
|
|
286
309
|
# indexes (a memory write changes them) after both; the model, the
|
|
287
310
|
# location and the session close the prompt (Gemma's tool declarations
|
|
288
311
|
# still follow them).
|
|
@@ -295,7 +318,7 @@ module Samagotchi
|
|
|
295
318
|
"System memories:\n#{indexes[:system]}"
|
|
296
319
|
].join("\n\n")
|
|
297
320
|
stable = [thinking_token + base, rg_guidance, system_identity_section, model_notes_section, explicit_memory_section,
|
|
298
|
-
project_specific_description, memory_sections]
|
|
321
|
+
project_specific_description, appended_section, memory_sections]
|
|
299
322
|
stable_text = stable.compact.join("\n")
|
|
300
323
|
volatile = [current_model, project_location, current_session].compact
|
|
301
324
|
return with_tail_declarations(stable_text, chat: chat, layers: layers) if volatile.empty?
|
|
@@ -464,26 +487,59 @@ module Samagotchi
|
|
|
464
487
|
|
|
465
488
|
# ── Project / rg helpers ───────────────────────────────────────────────────
|
|
466
489
|
|
|
467
|
-
#
|
|
490
|
+
# The AGENTS.md chain from the top of the git work tree down to the cwd
|
|
491
|
+
# (PromptFiles.agents_sections), one section per file with text.
|
|
468
492
|
def project_specific_description
|
|
469
493
|
return nil if skip_agent_description?
|
|
470
494
|
|
|
471
|
-
|
|
472
|
-
return nil
|
|
473
|
-
|
|
474
|
-
content = File.read(path, encoding: "UTF-8").strip
|
|
475
|
-
return nil if content.empty?
|
|
495
|
+
sections = PromptFiles.agents_sections(Dir.pwd)
|
|
496
|
+
return nil if sections.empty?
|
|
476
497
|
|
|
477
|
-
|
|
498
|
+
@section_files&.concat(sections.map(&:path))
|
|
499
|
+
sections.map(&:text).join("\n\n")
|
|
478
500
|
rescue StandardError
|
|
479
501
|
nil
|
|
480
502
|
end
|
|
481
503
|
|
|
482
|
-
|
|
504
|
+
# The appended files' text, right after AGENTS.md (the stable, cached
|
|
505
|
+
# part): config.yml's append_system_prompt: entries first (a relative
|
|
506
|
+
# one against the cwd, as AGENTS.md is found; a missing one is skipped),
|
|
507
|
+
# then --append-system-prompt's. A file the AGENTS.md chain holds, a
|
|
508
|
+
# repeat and an empty or vanished file are left out.
|
|
509
|
+
def appended_section
|
|
483
510
|
cwd = Dir.pwd
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
511
|
+
top = MemoryPaths.work_tree_root(cwd)
|
|
512
|
+
seen = Set.new((skip_agent_description? ? [] : PromptFiles.agents_paths(cwd)).map { |path| canonical_path(path) })
|
|
513
|
+
sections = []
|
|
514
|
+
appended_candidates(cwd).each do |path, source|
|
|
515
|
+
next unless seen.add?(canonical_path(path))
|
|
516
|
+
|
|
517
|
+
unless File.file?(path)
|
|
518
|
+
Log.debug(:config, "appended_prompt_file_missing", path: path, source: source)
|
|
519
|
+
next
|
|
520
|
+
end
|
|
521
|
+
text = PromptFiles.section(path, "Appended from #{PromptFiles.display_path(path, top: top)}:")
|
|
522
|
+
next unless text
|
|
523
|
+
|
|
524
|
+
@section_files&.push(path)
|
|
525
|
+
sections << text
|
|
526
|
+
end
|
|
527
|
+
sections.empty? ? nil : sections.join("\n\n")
|
|
528
|
+
rescue StandardError
|
|
529
|
+
nil
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# [absolute path, "config" | "cli"], in prompt order.
|
|
533
|
+
def appended_candidates(cwd)
|
|
534
|
+
@config_appended.map { |entry| [File.expand_path(entry, cwd), "config"] } +
|
|
535
|
+
@cli_appended.map { |path| [path, "cli"] }
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# The same file however it's spelled (/tmp vs /private/tmp on macOS).
|
|
539
|
+
def canonical_path(path)
|
|
540
|
+
File.realpath(path)
|
|
541
|
+
rescue SystemCallError
|
|
542
|
+
File.expand_path(path)
|
|
487
543
|
end
|
|
488
544
|
|
|
489
545
|
# Where the session runs and which project memory folder it uses. Only
|
|
@@ -566,7 +622,7 @@ module Samagotchi
|
|
|
566
622
|
end
|
|
567
623
|
|
|
568
624
|
def skip_agent_description?
|
|
569
|
-
Config.get("
|
|
625
|
+
Config.get("skip_agents_md") == true
|
|
570
626
|
end
|
|
571
627
|
|
|
572
628
|
def rg_available?
|
|
@@ -37,6 +37,8 @@ module Samagotchi
|
|
|
37
37
|
# @param memories [Array<String>] --memory: a new session's worker
|
|
38
38
|
# preloads them (an existing session keeps its own list)
|
|
39
39
|
# @param muted_memories [Array<String>] --mute: hidden from a new session
|
|
40
|
+
# @param appended_prompt_files [Array<String>] --append-system-prompt's
|
|
41
|
+
# files (absolute): a new session's setup carries them
|
|
40
42
|
# @param llm_context [Hash{Symbol => String}] --llm-context* as typed
|
|
41
43
|
# (LLMContextOverride.update): a new session starts with them; a
|
|
42
44
|
# resumed or attached one's worker gets a /llm-context, after the
|
|
@@ -48,9 +50,10 @@ module Samagotchi
|
|
|
48
50
|
# :failed when the --model switch didn't go through, or (input from a
|
|
49
51
|
# pipe) :turn_failed / :empty_answer / :unanswered (AttachedLoop#run)
|
|
50
52
|
def run(attach: nil, shared: false, resume: nil, prompt: nil, model: nil, no_interrupt: false, default_input: true,
|
|
51
|
-
memories: [], muted_memories: [], llm_context: {}, thinking: nil)
|
|
53
|
+
memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [])
|
|
52
54
|
client = connect(attach: attach, shared: shared, resume: resume, model: model,
|
|
53
|
-
memories: memories, muted_memories: muted_memories, llm_context: llm_context, thinking: thinking
|
|
55
|
+
memories: memories, muted_memories: muted_memories, llm_context: llm_context, thinking: thinking,
|
|
56
|
+
appended_prompt_files: appended_prompt_files)
|
|
54
57
|
commands = []
|
|
55
58
|
commands << "/model #{model}" if model && (attach || resume)
|
|
56
59
|
commands << LLMContextOverride.command_line(llm_context) if (attach || resume) && !llm_context.empty?
|
|
@@ -117,9 +120,11 @@ module Samagotchi
|
|
|
117
120
|
# @return [BridgeClient] a client for the live Bridge of the session
|
|
118
121
|
# @raise [Error]
|
|
119
122
|
def connect(attach: nil, shared: false, resume: nil, model: nil, state_dir: nil, wait: BRIDGE_WAIT,
|
|
120
|
-
memories: [], muted_memories: [], llm_context: {}, thinking: nil, err: $stderr)
|
|
123
|
+
memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], err: $stderr)
|
|
121
124
|
sd = state_dir || Session.default_state_dir
|
|
122
|
-
|
|
125
|
+
if attach || resume
|
|
126
|
+
warn_memory_flags_ignored(attach || resume, memories, muted_memories, err, appended_prompt_files: appended_prompt_files)
|
|
127
|
+
end
|
|
123
128
|
if attach
|
|
124
129
|
live = connect_existing(attach, sd)
|
|
125
130
|
return live if live
|
|
@@ -134,7 +139,8 @@ module Samagotchi
|
|
|
134
139
|
SessionManager.spawn_session(prompt: nil, model_name: model && model_ref(model), state_dir: sd,
|
|
135
140
|
memories: memories, muted_memories: muted_memories,
|
|
136
141
|
setup: SessionSetup.new(llm_context: llm_context.empty? ? nil : LLMContextOverride.update(nil, llm_context),
|
|
137
|
-
thinking: Thinking.session_level(thinking)
|
|
142
|
+
thinking: Thinking.session_level(thinking),
|
|
143
|
+
appended_prompt_files: appended_prompt_files))
|
|
138
144
|
end
|
|
139
145
|
client = BridgeClient.wait_for(session.id, session_dir: Session.session_dir(session.id, state_dir: sd), timeout: wait)
|
|
140
146
|
client || raise(Error, "the worker for session #{session.id} did not start its Bridge in time")
|
|
@@ -150,16 +156,19 @@ module Samagotchi
|
|
|
150
156
|
end
|
|
151
157
|
|
|
152
158
|
# An existing session's prompt is built from its own session fields:
|
|
153
|
-
# --memory/--mute given with --attach or
|
|
154
|
-
# line saying so (printed before the live
|
|
155
|
-
|
|
159
|
+
# --memory/--mute/--append-system-prompt given with --attach or
|
|
160
|
+
# --resume are ignored, with a line saying so (printed before the live
|
|
161
|
+
# region opens).
|
|
162
|
+
def warn_memory_flags_ignored(session_id, memories, muted_memories, err, appended_prompt_files: [])
|
|
156
163
|
flags = []
|
|
157
164
|
flags << "--memory" unless Array(memories).empty?
|
|
158
165
|
flags << "--mute" unless Array(muted_memories).empty?
|
|
166
|
+
flags << "--append-system-prompt" unless Array(appended_prompt_files).empty?
|
|
159
167
|
return if flags.empty?
|
|
160
168
|
|
|
161
169
|
verb = flags.size == 1 ? "applies" : "apply"
|
|
162
|
-
|
|
170
|
+
names = flags.size > 2 ? "#{flags[0..-2].join(", ")} and #{flags.last}" : flags.join(" and ")
|
|
171
|
+
err.puts "(#{names} #{verb} to a new session; #{session_id}'s prompt is already built)"
|
|
163
172
|
end
|
|
164
173
|
|
|
165
174
|
# @return [BridgeClient, nil] the running worker's, or nil when none runs
|
|
@@ -8,6 +8,7 @@ require_relative "../steer"
|
|
|
8
8
|
require_relative "../steer_cut"
|
|
9
9
|
require_relative "../memory_bundle/index_size"
|
|
10
10
|
require_relative "../prompt_note"
|
|
11
|
+
require_relative "../prompt_files"
|
|
11
12
|
|
|
12
13
|
module Samagotchi
|
|
13
14
|
class TerminalUI
|
|
@@ -99,8 +100,9 @@ module Samagotchi
|
|
|
99
100
|
# What a line submitted at the open prompt says became of it: a plain
|
|
100
101
|
# line goes in at the running turn's next step boundary (nothing cuts
|
|
101
102
|
# unless the message asked for it), /cut's cutting now or once the
|
|
102
|
-
# thinking passes
|
|
103
|
-
#
|
|
103
|
+
# thinking passes the model's cut wait (the run's cut_after; the global
|
|
104
|
+
# steer.cut_after when the worker didn't say), and /queue's waiting for
|
|
105
|
+
# the end. Both TUIs and chi send --cut print the same words
|
|
104
106
|
# (SteerCut#cut_for_steer's answer).
|
|
105
107
|
NEXT_STEP_NOTE = "(goes in at the next step)"
|
|
106
108
|
CUT_NOW_NOTE = "(cut in now)"
|
|
@@ -116,17 +118,20 @@ module Samagotchi
|
|
|
116
118
|
STEER_CHOICE = /\A(#{CUT_COMMAND}|#{QUEUE_COMMAND})(?:\s+(.*))?\z/m
|
|
117
119
|
|
|
118
120
|
# @param outcome [Symbol] SteerCut#cut_for_steer's answer: :now, :waits or :off
|
|
121
|
+
# @param after [Integer, nil] the run's cut wait (the model's own
|
|
122
|
+
# SteerCutAfter): the seconds a :waits waits for; the global
|
|
123
|
+
# steer.cut_after when not given.
|
|
119
124
|
# @return [String] the dim line to show for a /cut
|
|
120
|
-
def self.steer_cut_note(outcome)
|
|
125
|
+
def self.steer_cut_note(outcome, after = nil)
|
|
121
126
|
case outcome
|
|
122
127
|
when :now then CUT_NOW_NOTE
|
|
123
|
-
when :waits then format(CUT_WAITS_NOTE, SteerCut.cut_after)
|
|
128
|
+
when :waits then format(CUT_WAITS_NOTE, after || SteerCut.cut_after)
|
|
124
129
|
else CUT_OFF_NOTE
|
|
125
130
|
end
|
|
126
131
|
end
|
|
127
132
|
|
|
128
133
|
# The same words, for a UI that includes the module.
|
|
129
|
-
def steer_cut_note(outcome) = Formatting.steer_cut_note(outcome)
|
|
134
|
+
def steer_cut_note(outcome, after = nil) = Formatting.steer_cut_note(outcome, after)
|
|
130
135
|
|
|
131
136
|
# The choice a mid-turn line carries, as [:cut, "TEXT"] or
|
|
132
137
|
# [:queue, "TEXT"]; nil for any other line. The text is "" when the
|
|
@@ -400,6 +405,16 @@ module Samagotchi
|
|
|
400
405
|
"#{"~" if source.to_s == "estimate"}#{number} tok/s"
|
|
401
406
|
end
|
|
402
407
|
|
|
408
|
+
# The prompt files (absolute paths) named as the prompt names them:
|
|
409
|
+
# relative to this checkout's top, else with ~; nil with none.
|
|
410
|
+
def prompt_files_text(paths)
|
|
411
|
+
list = Array(paths).map(&:to_s).reject(&:empty?)
|
|
412
|
+
return nil if list.empty?
|
|
413
|
+
|
|
414
|
+
top = MemoryPaths.work_tree_root(Dir.pwd)
|
|
415
|
+
list.map { |path| PromptFiles.display_path(path, top: top) }.join(", ")
|
|
416
|
+
end
|
|
417
|
+
|
|
403
418
|
# "~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)":
|
|
404
419
|
# the memory indexes the session's prompt holds (the snapshot's
|
|
405
420
|
# memory_index, symbol or string keys); "" without one. As ctx.js
|
|
@@ -596,6 +611,9 @@ module Samagotchi
|
|
|
596
611
|
# The model notes the session's prompt carried (Engine#prompt_notes).
|
|
597
612
|
notes = PromptNote.text(snapshot[:prompt_notes])
|
|
598
613
|
lines << "model notes: #{notes}" unless notes.empty?
|
|
614
|
+
# The files whose text is in the prompt (SystemPrompt#prompt_files).
|
|
615
|
+
files = prompt_files_text(snapshot[:prompt_files])
|
|
616
|
+
lines << "prompt files: #{files}" if files
|
|
599
617
|
llm_context = llm_context_stats_text(snapshot[:llm_context])
|
|
600
618
|
lines << "llm context: #{llm_context}" if llm_context
|
|
601
619
|
thinking = setting_stats_text(snapshot[:thinking])
|
|
@@ -92,8 +92,12 @@ module Samagotchi
|
|
|
92
92
|
# (LLMContextOverride.update): set on the new or resumed session
|
|
93
93
|
# @param thinking [String, nil] --thinking as typed: the new or resumed
|
|
94
94
|
# session's own level (default unsets it)
|
|
95
|
+
# @param appended_prompt_files [Array<String>] --append-system-prompt's
|
|
96
|
+
# files (absolute): a new session saves them; a resumed one's own come
|
|
97
|
+
# first, and the merge goes only to the Engine (as --memory's does)
|
|
95
98
|
def initialize(prompt: nil, client: nil, host_registry: nil, profile: nil, session_id: nil, no_interrupt: false, no_default_input: false, model_name: nil, memories: [], muted_memories: [], non_interactive: false, surface: nil,
|
|
96
|
-
spinner_tick_interval: AttachedView::TICK_INTERVAL, spinner_clock: nil, scratch: false, llm_context: {}, thinking: nil
|
|
99
|
+
spinner_tick_interval: AttachedView::TICK_INTERVAL, spinner_clock: nil, scratch: false, llm_context: {}, thinking: nil,
|
|
100
|
+
appended_prompt_files: [])
|
|
97
101
|
@scratch = scratch
|
|
98
102
|
@thinking_word = thinking
|
|
99
103
|
@prompt = prompt
|
|
@@ -142,9 +146,11 @@ module Samagotchi
|
|
|
142
146
|
# session started attached with mutes keeps them here.
|
|
143
147
|
@requested_memories = Array(memories)
|
|
144
148
|
@muted_memory_names = Array(muted_memories)
|
|
149
|
+
@appended_prompt_files = Array(appended_prompt_files)
|
|
145
150
|
if @resume_session
|
|
146
151
|
@requested_memories = (@resume_session.preloaded_memory_names + @requested_memories).uniq
|
|
147
152
|
@muted_memory_names = (@resume_session.muted_memory_names + @muted_memory_names).uniq
|
|
153
|
+
@appended_prompt_files = (@resume_session.appended_prompt_files + @appended_prompt_files).uniq
|
|
148
154
|
end
|
|
149
155
|
# Every terminal write goes through the surface. The REPL swaps in a
|
|
150
156
|
# live region when the terminal can show one (#assist_loop), unless it
|
|
@@ -178,6 +184,7 @@ module Samagotchi
|
|
|
178
184
|
model_typed: no_default ? @resumed_model_typed : nil,
|
|
179
185
|
memories: @requested_memories,
|
|
180
186
|
muted_memories: @muted_memory_names,
|
|
187
|
+
appended_prompt_files: @appended_prompt_files,
|
|
181
188
|
recap: scratch ? nil : recap_config,
|
|
182
189
|
scratch: scratch,
|
|
183
190
|
reminders: {
|
|
@@ -258,7 +265,8 @@ module Samagotchi
|
|
|
258
265
|
working_directory: Dir.pwd,
|
|
259
266
|
preloaded_memory_names: @requested_memories,
|
|
260
267
|
muted_memory_names: @muted_memory_names,
|
|
261
|
-
scratch: @scratch
|
|
268
|
+
scratch: @scratch,
|
|
269
|
+
setup: SessionSetup.new(appended_prompt_files: @appended_prompt_files)
|
|
262
270
|
))
|
|
263
271
|
# --llm-context*: the session's own values, over a resumed one's.
|
|
264
272
|
# Saved at once on a resumed one: leaving before a turn keeps them.
|
|
@@ -628,7 +636,7 @@ module Samagotchi
|
|
|
628
636
|
# Run one REPL turn (a prompt, or a continue/reminder turn with nil) through
|
|
629
637
|
# Engine#run_turn, rendering via @renderer.
|
|
630
638
|
# @param images [Array<Hash>] the prompt's `@path` images ({path:})
|
|
631
|
-
def run_engine_turn(session, prompt, continue: false, max_iterations: IterationLimit.for, images: [])
|
|
639
|
+
def run_engine_turn(session, prompt, continue: false, max_iterations: IterationLimit.for, images: [], resumed: false, resume_from: nil)
|
|
632
640
|
cancellation_controller = CancellationController.new
|
|
633
641
|
@active_cancel_controller = cancellation_controller
|
|
634
642
|
@renderer.begin_turn
|
|
@@ -646,7 +654,9 @@ module Samagotchi
|
|
|
646
654
|
cancel_controller: cancellation_controller,
|
|
647
655
|
pending_input: method(:drain_steering),
|
|
648
656
|
continue: continue,
|
|
649
|
-
images: images
|
|
657
|
+
images: images,
|
|
658
|
+
resumed: resumed,
|
|
659
|
+
resume_from: resume_from
|
|
650
660
|
)
|
|
651
661
|
end
|
|
652
662
|
rescue Interrupt
|
|
@@ -774,9 +784,11 @@ module Samagotchi
|
|
|
774
784
|
end
|
|
775
785
|
return show_command_result(answer) unless answer.resume
|
|
776
786
|
|
|
787
|
+
# The run the offer's turn started: the Continue keeps its reasoning.
|
|
788
|
+
resume_from = @turn_flow.resume_from
|
|
777
789
|
@turn_flow.before_continue_turn
|
|
778
790
|
begin
|
|
779
|
-
result = run_engine_turn(session, nil, continue: true)
|
|
791
|
+
result = run_engine_turn(session, nil, continue: true, resumed: true, resume_from: resume_from)
|
|
780
792
|
rescue LLM::ProviderError
|
|
781
793
|
# The renderer showed the failure (:turn_failed).
|
|
782
794
|
@surface.commit(turn_end_hint("continue prompt preserved"))
|
|
@@ -1199,7 +1211,7 @@ module Samagotchi
|
|
|
1199
1211
|
@pending_input_queue.push(text)
|
|
1200
1212
|
# With no session running the cut is :off; the text still waits for
|
|
1201
1213
|
# the running turn's next step.
|
|
1202
|
-
@surface.commit(steer_cut_note(@engine.cut_for_steer(nil, epoch: epoch)))
|
|
1214
|
+
@surface.commit(steer_cut_note(@engine.cut_for_steer(nil, epoch: epoch), @engine.steer_cut_after))
|
|
1203
1215
|
true
|
|
1204
1216
|
end
|
|
1205
1217
|
|
|
@@ -46,7 +46,11 @@ module Samagotchi
|
|
|
46
46
|
status: tool_activity_status(result, tool_name, command: call.is_a?(Hash) ? call[:content] : nil)
|
|
47
47
|
}
|
|
48
48
|
# A grep that found nothing: ok, and the web row says "no match".
|
|
49
|
-
|
|
49
|
+
# Not a hand-over: an "exit: 1" in its output tail is the command's.
|
|
50
|
+
if event[:status] == "ok" && tool_name == Tools::Execute::NAME && !handed_over?(tool_name, result) &&
|
|
51
|
+
execute_failed?(result.to_s)
|
|
52
|
+
event[:no_match] = true
|
|
53
|
+
end
|
|
50
54
|
title = tool_title(tool_name, call, cwd: cwd)
|
|
51
55
|
event[:title] = title if title
|
|
52
56
|
# The name the model used when an alias ran a built-in (called_as: "bash"
|
|
@@ -214,6 +218,8 @@ module Samagotchi
|
|
|
214
218
|
return "stopped" if text == Tools::Execute::NOT_RUN_ON_STOP
|
|
215
219
|
return "stopped" if tool_name == Tools::Execute::NAME && text.start_with?(Tools::Execute::STOPPED_BY_USER)
|
|
216
220
|
return "error" if text.start_with?("Error:")
|
|
221
|
+
# Still running, as a task: its output tail may hold any "exit: N".
|
|
222
|
+
return "ok" if handed_over?(tool_name, text)
|
|
217
223
|
if tool_name == Tools::Execute::NAME && execute_failed?(text)
|
|
218
224
|
return no_match?(text, command) ? "ok" : "error"
|
|
219
225
|
end
|
|
@@ -222,6 +228,17 @@ module Samagotchi
|
|
|
222
228
|
"ok"
|
|
223
229
|
end
|
|
224
230
|
|
|
231
|
+
# An execute still running at its limit, handed over as a task
|
|
232
|
+
# (Tools::Execute::HANDED_OVER).
|
|
233
|
+
def handed_over?(tool_name, text)
|
|
234
|
+
return false unless tool_name == Tools::Execute::NAME && text.to_s.start_with?(Tools::Execute::HANDED_OVER)
|
|
235
|
+
|
|
236
|
+
# The first line only, scrubbed: a regexp raises on bytes that aren't UTF-8.
|
|
237
|
+
text.to_s.each_line.first.to_s.scrub.match?(HANDED_OVER_START)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
HANDED_OVER_START = /\A#{Regexp.escape(Tools::Execute::HANDED_OVER)} \d+s limit:/
|
|
241
|
+
|
|
225
242
|
def execute_failed?(text)
|
|
226
243
|
code = execute_exit_code(text)
|
|
227
244
|
!code.nil? && code != "0"
|
|
@@ -20,7 +20,7 @@ module Samagotchi
|
|
|
20
20
|
TOOL_SCHEMAS = [
|
|
21
21
|
{
|
|
22
22
|
name: "execute",
|
|
23
|
-
description: "Run any shell command in a working directory (defaults to the project root) and see stdout, stderr, and exit code. Large output may be truncated to a head+tail preview with metadata. Don't background with &; use task_create.",
|
|
23
|
+
description: "Run any shell command in a working directory (defaults to the project root) and see stdout, stderr, and exit code. Large output may be truncated to a head+tail preview with metadata. A command still running at the time limit keeps running as a background task (by default): the result gives its task_id, and task_wait waits for it. Don't background with &; use task_create.",
|
|
24
24
|
parameters: {
|
|
25
25
|
type: "object",
|
|
26
26
|
properties: {
|