samagotchi 0.2.0 → 0.4.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 +198 -1
- data/README.md +56 -4
- data/bin/chi +118 -50
- data/docs/cli.md +184 -9
- data/docs/configuration.md +333 -47
- data/docs/desktop.md +45 -4
- data/docs/guardrails.md +11 -0
- data/docs/hooks.md +208 -5
- data/docs/plugins.md +68 -2
- data/docs/releasing.md +23 -13
- data/docs/sessions.md +45 -17
- data/lib/samagotchi/answer_display.rb +95 -0
- data/lib/samagotchi/archive_store.rb +90 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +342 -0
- data/lib/samagotchi/bootstrap/probe.rb +262 -0
- data/lib/samagotchi/bootstrap_command.rb +347 -0
- data/lib/samagotchi/bridge/pending_card.rb +89 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +15 -3
- data/lib/samagotchi/bridge.rb +13 -1
- data/lib/samagotchi/bridge_client.rb +6 -2
- data/lib/samagotchi/bundles/check-in/manifest.yml +10 -0
- data/lib/samagotchi/bundles/check-in/plugin.rb +244 -0
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +531 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +14 -0
- data/lib/samagotchi/bundles/source-links/source_links.md +5 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +10 -6
- data/lib/samagotchi/bundles/system/delegated.md +6 -7
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/self_map.md +8 -2
- data/lib/samagotchi/client.rb +81 -19
- data/lib/samagotchi/commands/registry.rb +8 -0
- data/lib/samagotchi/config.rb +252 -48
- data/lib/samagotchi/desktop/macos/App.swift +12 -8
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +17 -9
- data/lib/samagotchi/desktop/macos/Images.swift +113 -0
- data/lib/samagotchi/desktop/macos/Info.plist.erb +6 -0
- data/lib/samagotchi/desktop/macos/Panel.swift +180 -25
- data/lib/samagotchi/desktop/macos.rb +59 -8
- data/lib/samagotchi/desktop_command.rb +6 -3
- data/lib/samagotchi/edit_preview.rb +82 -0
- data/lib/samagotchi/empty_answer_retry.rb +43 -0
- data/lib/samagotchi/engine.rb +434 -140
- data/lib/samagotchi/gem_update.rb +89 -0
- data/lib/samagotchi/guardrails/approval.rb +35 -4
- data/lib/samagotchi/guardrails/load_failures.rb +9 -3
- data/lib/samagotchi/guardrails/scratch_writes.rb +40 -0
- data/lib/samagotchi/guardrails.rb +1 -0
- data/lib/samagotchi/hooks/registry.rb +24 -5
- data/lib/samagotchi/host_registry.rb +9 -12
- data/lib/samagotchi/idle_client.rb +24 -15
- data/lib/samagotchi/idle_recap.rb +5 -1
- data/lib/samagotchi/idle_reminders.rb +2 -2
- data/lib/samagotchi/image_store.rb +10 -6
- data/lib/samagotchi/kernel_loop.rb +73 -94
- data/lib/samagotchi/live_versions.rb +59 -0
- data/lib/samagotchi/llm/api_key.rb +41 -0
- data/lib/samagotchi/llm/chat_loop.rb +132 -29
- data/lib/samagotchi/llm/errors.rb +41 -9
- data/lib/samagotchi/llm/http.rb +57 -17
- data/lib/samagotchi/llm/openai_chat.rb +17 -30
- data/lib/samagotchi/log_subscriber.rb +18 -3
- data/lib/samagotchi/memory_bundle/installer.rb +65 -63
- data/lib/samagotchi/memory_bundle/provenance.rb +51 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +157 -0
- data/lib/samagotchi/memory_bundle/status.rb +4 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +81 -53
- data/lib/samagotchi/model_profile.rb +24 -1
- data/lib/samagotchi/plugin/context.rb +22 -1
- data/lib/samagotchi/plugin/sessions.rb +3 -1
- data/lib/samagotchi/prompt.rb +4 -2
- data/lib/samagotchi/reminder_store.rb +1 -9
- data/lib/samagotchi/reply_wait.rb +126 -0
- data/lib/samagotchi/sampling_settings.rb +58 -0
- data/lib/samagotchi/self_report.rb +18 -3
- data/lib/samagotchi/send_command.rb +252 -11
- data/lib/samagotchi/session.rb +52 -11
- data/lib/samagotchi/session_archive_command.rb +107 -0
- data/lib/samagotchi/session_commands.rb +46 -7
- data/lib/samagotchi/session_manager.rb +115 -25
- data/lib/samagotchi/session_metrics.rb +222 -106
- data/lib/samagotchi/steer.rb +72 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +57 -28
- data/lib/samagotchi/terminal_ui/event_renderer.rb +21 -11
- data/lib/samagotchi/terminal_ui/formatting.rb +40 -8
- data/lib/samagotchi/terminal_ui/input_support.rb +7 -19
- data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
- data/lib/samagotchi/terminal_ui.rb +134 -247
- data/lib/samagotchi/text_diff.rb +181 -0
- data/lib/samagotchi/thinking.rb +115 -0
- data/lib/samagotchi/tool_activity.rb +3 -1
- data/lib/samagotchi/tool_runner.rb +34 -1
- data/lib/samagotchi/tools/ask_user_question.rb +41 -33
- data/lib/samagotchi/tools/builtins.rb +15 -4
- data/lib/samagotchi/tools/delegate_wait.rb +26 -69
- data/lib/samagotchi/tools/edit.rb +23 -9
- data/lib/samagotchi/tools/execute.rb +52 -14
- data/lib/samagotchi/tools/task_runtime.rb +19 -0
- data/lib/samagotchi/tools/task_wait.rb +27 -3
- data/lib/samagotchi/tools/write.rb +4 -0
- data/lib/samagotchi/turn_flow.rb +12 -2
- data/lib/samagotchi/turn_note.rb +60 -6
- data/lib/samagotchi/update_command.rb +308 -0
- data/lib/samagotchi/update_hint.rb +59 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +7 -9
- data/lib/samagotchi/web/app.rb +91 -7
- data/lib/samagotchi/web/message_parts.rb +8 -3
- data/lib/samagotchi/web/public/activity.js +13 -1
- data/lib/samagotchi/web/public/annotate_presets.js +26 -0
- data/lib/samagotchi/web/public/annotations.js +13 -0
- data/lib/samagotchi/web/public/app.js +472 -111
- data/lib/samagotchi/web/public/card.js +5 -3
- data/lib/samagotchi/web/public/chat_view.js +13 -1
- data/lib/samagotchi/web/public/copy.js +20 -4
- data/lib/samagotchi/web/public/ctx.js +15 -0
- data/lib/samagotchi/web/public/data.js +23 -6
- data/lib/samagotchi/web/public/diff_view.js +58 -0
- data/lib/samagotchi/web/public/format.js +9 -0
- data/lib/samagotchi/web/public/index.html +60 -3
- data/lib/samagotchi/web/public/notify.js +175 -0
- data/lib/samagotchi/web/public/question_card.js +5 -2
- data/lib/samagotchi/web/public/sessions_list.js +7 -0
- data/lib/samagotchi/web/public/timing.js +39 -14
- data/lib/samagotchi/web/public/turn_events.js +75 -5
- data/lib/samagotchi/web/public/turn_view.js +49 -8
- data/lib/samagotchi/web/server.rb +8 -4
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_summary.rb +24 -1
- data/lib/samagotchi/worker.rb +16 -4
- metadata +31 -1
|
@@ -22,9 +22,25 @@ module Samagotchi
|
|
|
22
22
|
|
|
23
23
|
def self.call(content, path:, start_line: nil, end_line: nil)
|
|
24
24
|
path = ToolPath.normalize(path)
|
|
25
|
+
result = apply(content, path: path, start_line: start_line, end_line: end_line)
|
|
26
|
+
return result if result.is_a?(String)
|
|
27
|
+
|
|
28
|
+
updated, message = result
|
|
29
|
+
File.write(path, updated)
|
|
30
|
+
message
|
|
31
|
+
rescue => e
|
|
32
|
+
"Error: #{e.message}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The edit without the write: [updated, message] or "Error: …", with
|
|
36
|
+
# exactly the strings call returns. +read+ reads the file, so a dry run
|
|
37
|
+
# (EditPreview) can reuse a copy it already has; each mode keeps its own
|
|
38
|
+
# check order (exact: tags first; range: file not found first).
|
|
39
|
+
def self.apply(content, path:, start_line: nil, end_line: nil, read: ->(p) { File.read(p) })
|
|
40
|
+
path = ToolPath.normalize(path)
|
|
25
41
|
|
|
26
42
|
if range_requested?(start_line, end_line)
|
|
27
|
-
return
|
|
43
|
+
return apply_range_mode(content, path: path, start_line: start_line, end_line: end_line, read: read)
|
|
28
44
|
end
|
|
29
45
|
|
|
30
46
|
old_text = extract_tag(content, "old")
|
|
@@ -35,7 +51,7 @@ module Samagotchi
|
|
|
35
51
|
return "Error: <old> block is empty" if old_text.empty?
|
|
36
52
|
return "Error: file not found: #{path}" unless File.exist?(path)
|
|
37
53
|
|
|
38
|
-
source =
|
|
54
|
+
source = read.(path)
|
|
39
55
|
count = count_occurrences(source, old_text)
|
|
40
56
|
|
|
41
57
|
return "Error: old text not found in #{path}" if count == 0
|
|
@@ -43,13 +59,12 @@ module Samagotchi
|
|
|
43
59
|
|
|
44
60
|
idx = source.index(old_text)
|
|
45
61
|
updated = source[0, idx] + new_text + source[idx + old_text.length..]
|
|
46
|
-
|
|
47
|
-
"Edited #{path}: replaced #{old_text.bytesize} bytes with #{new_text.bytesize} bytes"
|
|
62
|
+
[updated, "Edited #{path}: replaced #{old_text.bytesize} bytes with #{new_text.bytesize} bytes"]
|
|
48
63
|
rescue => e
|
|
49
64
|
"Error: #{e.message}"
|
|
50
65
|
end
|
|
51
66
|
|
|
52
|
-
def self.
|
|
67
|
+
def self.apply_range_mode(content, path:, start_line:, end_line:, read:)
|
|
53
68
|
return "Error: file not found: #{path}" unless File.exist?(path)
|
|
54
69
|
|
|
55
70
|
new_text = extract_tag(content, "new")
|
|
@@ -59,7 +74,7 @@ module Samagotchi
|
|
|
59
74
|
return start_num if start_num.is_a?(String)
|
|
60
75
|
return "Error: start_line must be provided for range edits" if start_num.nil?
|
|
61
76
|
|
|
62
|
-
source =
|
|
77
|
+
source = read.(path)
|
|
63
78
|
lines = source.lines
|
|
64
79
|
total_lines = lines.length
|
|
65
80
|
|
|
@@ -102,15 +117,14 @@ module Samagotchi
|
|
|
102
117
|
# first suffix line isn't concatenated onto the last replacement line.
|
|
103
118
|
normalized = (!new_text.empty? && !suffix.empty? && !new_text.end_with?("\n")) ? new_text + "\n" : new_text
|
|
104
119
|
updated = prefix + normalized + suffix
|
|
105
|
-
File.write(path, updated)
|
|
106
120
|
|
|
107
121
|
replaced_lines = (end_value - start_num) + 1
|
|
108
122
|
new_line_count = new_text.lines.length
|
|
109
|
-
"Edited #{path}: replaced lines #{start_num}-#{end_value} (#{replaced_lines} lines) with #{new_line_count} lines#{clamp_note}"
|
|
123
|
+
[updated, "Edited #{path}: replaced lines #{start_num}-#{end_value} (#{replaced_lines} lines) with #{new_line_count} lines#{clamp_note}"]
|
|
110
124
|
rescue => e
|
|
111
125
|
"Error: #{e.message}"
|
|
112
126
|
end
|
|
113
|
-
private_class_method :
|
|
127
|
+
private_class_method :apply_range_mode
|
|
114
128
|
|
|
115
129
|
# Count non-overlapping literal occurrences of +needle+ in +haystack+.
|
|
116
130
|
def self.count_occurrences(haystack, needle)
|
|
@@ -18,16 +18,22 @@ module Samagotchi
|
|
|
18
18
|
TIMEOUT_SEC = 30
|
|
19
19
|
STOP_GRACE_SEC = 1.0
|
|
20
20
|
STOP_POLL_INTERVAL_SEC = 0.05
|
|
21
|
+
WAIT_SLICE_SEC = 0.2
|
|
22
|
+
NOT_RUN_ON_STOP = "Error: not run, the user stopped the turn"
|
|
21
23
|
|
|
22
24
|
def self.name = NAME
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
# @param cancelled [#call] true once the user stopped the turn: the
|
|
27
|
+
# command is killed (or not started) and its output so far returned
|
|
28
|
+
def self.call(command, cwd: nil, cancelled: -> { false })
|
|
29
|
+
return NOT_RUN_ON_STOP if cancelled.call
|
|
30
|
+
|
|
25
31
|
command = command.strip
|
|
26
32
|
resolved_cwd = resolve_cwd(cwd)
|
|
27
33
|
return "Error: cwd not found: #{resolved_cwd}" unless resolved_cwd
|
|
28
34
|
|
|
29
35
|
timeout_sec = timeout_seconds
|
|
30
|
-
stdout, stderr, status = run_command(command, timeout_sec: timeout_sec, cwd: resolved_cwd)
|
|
36
|
+
stdout, stderr, status = run_command(command, timeout_sec: timeout_sec, cwd: resolved_cwd, cancelled: cancelled)
|
|
31
37
|
|
|
32
38
|
stdout_block = output_block("stdout", stdout)
|
|
33
39
|
stderr_block = output_block("stderr", stderr)
|
|
@@ -45,19 +51,29 @@ module Samagotchi
|
|
|
45
51
|
rescue CommandTimedOut => e
|
|
46
52
|
# Keep the "Error:" first line (callers classify on it) and return
|
|
47
53
|
# whatever the command printed before it was killed.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
parts.compact.join("\n")
|
|
54
|
+
stopped_result("Error: command timed out after #{timeout_sec}s", e)
|
|
55
|
+
rescue CommandCancelled => e
|
|
56
|
+
stopped_result("Error: command stopped by the user after #{e.elapsed.round}s (killed; rerun it if still needed)", e)
|
|
52
57
|
rescue => e
|
|
53
58
|
"Error: #{e.message}"
|
|
54
59
|
end
|
|
55
60
|
|
|
56
|
-
def self.
|
|
61
|
+
def self.stopped_result(first_line, error)
|
|
62
|
+
parts = [first_line]
|
|
63
|
+
parts << output_block("stdout", error.stdout)
|
|
64
|
+
parts << output_block("stderr", error.stderr)
|
|
65
|
+
parts.compact.join("\n")
|
|
66
|
+
end
|
|
67
|
+
private_class_method :stopped_result
|
|
68
|
+
|
|
69
|
+
def self.run_command(command, timeout_sec:, cwd:, cancelled: -> { false })
|
|
57
70
|
stdout_text = ""
|
|
58
71
|
stderr_text = ""
|
|
59
72
|
status = nil
|
|
60
73
|
timed_out = false
|
|
74
|
+
was_cancelled = false
|
|
75
|
+
started = monotonic_time
|
|
76
|
+
deadline = started + timeout_sec
|
|
61
77
|
|
|
62
78
|
Open3.popen3(command, chdir: cwd, pgroup: true) do |stdin, stdout, stderr, wait_thr|
|
|
63
79
|
stdin.close
|
|
@@ -65,14 +81,24 @@ module Samagotchi
|
|
|
65
81
|
stderr_reader = reader_thread_for(stderr)
|
|
66
82
|
|
|
67
83
|
begin
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
until wait_thr.join(WAIT_SLICE_SEC)
|
|
85
|
+
if cancelled.call
|
|
86
|
+
was_cancelled = true
|
|
87
|
+
break
|
|
88
|
+
elsif monotonic_time > deadline
|
|
89
|
+
timed_out = true
|
|
90
|
+
break
|
|
91
|
+
end
|
|
75
92
|
end
|
|
93
|
+
terminate_process_tree(wait_thr.pid) if timed_out || was_cancelled
|
|
94
|
+
wait_thr.join
|
|
95
|
+
status = wait_thr.value
|
|
96
|
+
rescue Exception # rubocop:disable Lint/RescueException -- kill the child, then re-raise
|
|
97
|
+
# An Interrupt (or anything else) mid-wait: the child is in its own
|
|
98
|
+
# process group, so it never saw the SIGINT. Kill it, or the reads
|
|
99
|
+
# below block until it closes stdout.
|
|
100
|
+
terminate_process_tree(wait_thr.pid)
|
|
101
|
+
raise
|
|
76
102
|
ensure
|
|
77
103
|
# Read the buffered output before closing the pipes. For a process
|
|
78
104
|
# that exits almost instantly (e.g. `echo hello`), the background
|
|
@@ -86,6 +112,7 @@ module Samagotchi
|
|
|
86
112
|
end
|
|
87
113
|
|
|
88
114
|
raise CommandTimedOut.new(stdout_text, stderr_text) if timed_out
|
|
115
|
+
raise CommandCancelled.new(stdout_text, stderr_text, monotonic_time - started) if was_cancelled
|
|
89
116
|
|
|
90
117
|
[stdout_text, stderr_text, status]
|
|
91
118
|
end
|
|
@@ -209,6 +236,17 @@ module Samagotchi
|
|
|
209
236
|
super("command timed out")
|
|
210
237
|
end
|
|
211
238
|
end
|
|
239
|
+
|
|
240
|
+
class CommandCancelled < StandardError
|
|
241
|
+
attr_reader :stdout, :stderr, :elapsed
|
|
242
|
+
|
|
243
|
+
def initialize(stdout, stderr, elapsed)
|
|
244
|
+
@stdout = stdout
|
|
245
|
+
@stderr = stderr
|
|
246
|
+
@elapsed = elapsed
|
|
247
|
+
super("command stopped by the user")
|
|
248
|
+
end
|
|
249
|
+
end
|
|
212
250
|
end
|
|
213
251
|
end
|
|
214
252
|
end
|
|
@@ -94,6 +94,25 @@ module Samagotchi
|
|
|
94
94
|
end.compact.sort_by { |record| record["created_at"].to_s }.reverse
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# The tasks this conversation's task_create calls started that are
|
|
98
|
+
# still running. Only task_create results count: records are shared by
|
|
99
|
+
# every session in the worker's cwd, and task_list shows them all.
|
|
100
|
+
# A native tool_response joins its calls' results with "---".
|
|
101
|
+
# @return [Array<Hash>] {id:, command:}, oldest first
|
|
102
|
+
def running_created_in(messages)
|
|
103
|
+
ids = Array(messages).flat_map do |message|
|
|
104
|
+
next [] unless (message[:role] || message["role"]).to_s == "tool_response"
|
|
105
|
+
|
|
106
|
+
(message[:content] || message["content"]).to_s.split("\n\n---\n\n").filter_map do |block|
|
|
107
|
+
block[/\A\[task_create\]\ntask_id: (\S+)/, 1]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
ids.uniq.filter_map do |id|
|
|
111
|
+
record, _error = get_record(id)
|
|
112
|
+
{ id: id, command: record["command"].to_s } if record&.fetch("status") == "running"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
97
116
|
def get_record(task_id)
|
|
98
117
|
record = load_record(task_id)
|
|
99
118
|
return [nil, "Error: task not found: #{task_id}"] unless record
|
|
@@ -14,7 +14,10 @@ module Samagotchi
|
|
|
14
14
|
|
|
15
15
|
def self.name = NAME
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
# @param cancelled [#call] true once the user stopped the turn: the
|
|
18
|
+
# wait returns then, and the task keeps running
|
|
19
|
+
def self.call(task_id, timeout: TIMEOUT_DEFAULT, tail_lines: TAIL_LINES_DEFAULT, done_pattern: nil,
|
|
20
|
+
cancelled: -> { false })
|
|
18
21
|
timeout = TIMEOUT_DEFAULT if blank?(timeout)
|
|
19
22
|
tail_lines = TAIL_LINES_DEFAULT if blank?(tail_lines)
|
|
20
23
|
timeout = timeout.to_i
|
|
@@ -27,7 +30,8 @@ module Samagotchi
|
|
|
27
30
|
return completion_pattern if completion_pattern.is_a?(String)
|
|
28
31
|
|
|
29
32
|
task_id = task_id.to_s.strip
|
|
30
|
-
|
|
33
|
+
started = monotonic_time
|
|
34
|
+
deadline = started + timeout
|
|
31
35
|
|
|
32
36
|
loop do
|
|
33
37
|
record, error = TaskRuntime.get_record(task_id)
|
|
@@ -41,6 +45,8 @@ module Samagotchi
|
|
|
41
45
|
return format_response(record, wait_result: "pattern_matched", output_tail: output_tail) if completion_pattern.match?(output_tail)
|
|
42
46
|
end
|
|
43
47
|
|
|
48
|
+
return cancelled_response(task_id, tail_lines, started) if cancelled.call
|
|
49
|
+
|
|
44
50
|
break if monotonic_time > deadline
|
|
45
51
|
sleep(POLL_INTERVAL)
|
|
46
52
|
end
|
|
@@ -55,7 +61,24 @@ module Samagotchi
|
|
|
55
61
|
"Error: #{e.message}"
|
|
56
62
|
end
|
|
57
63
|
|
|
58
|
-
|
|
64
|
+
# The task may have finished between the poll and the Stop: then the
|
|
65
|
+
# normal finished result. Only a task still running says so.
|
|
66
|
+
def self.cancelled_response(task_id, tail_lines, started)
|
|
67
|
+
record, error = TaskRuntime.get_record(task_id)
|
|
68
|
+
return "Error: #{error}" if error
|
|
69
|
+
return format_response(record) unless record.fetch("status") == "running"
|
|
70
|
+
|
|
71
|
+
output_tail = TaskRuntime.output_tail_lines(record.fetch("output_path"), tail_lines)
|
|
72
|
+
note = [
|
|
73
|
+
"note: the user stopped the turn while waiting; the task is STILL RUNNING.",
|
|
74
|
+
" To keep waiting: task_wait #{task_id}. If it is stuck or no longer needed: task_stop #{task_id}.",
|
|
75
|
+
"waited: #{(monotonic_time - started).round}s"
|
|
76
|
+
]
|
|
77
|
+
format_response(record, wait_result: "canceled", output_tail: output_tail, extra: note)
|
|
78
|
+
end
|
|
79
|
+
private_class_method :cancelled_response
|
|
80
|
+
|
|
81
|
+
def self.format_response(record, wait_result: nil, output_tail: nil, extra: [])
|
|
59
82
|
lines = [
|
|
60
83
|
"task_id: #{record.fetch("id")}",
|
|
61
84
|
"status: #{record.fetch("status")}",
|
|
@@ -66,6 +89,7 @@ module Samagotchi
|
|
|
66
89
|
return lines.join("\n") unless wait_result
|
|
67
90
|
|
|
68
91
|
lines << "wait_result: #{wait_result}"
|
|
92
|
+
lines.concat(extra)
|
|
69
93
|
lines << "output_tail:"
|
|
70
94
|
lines << output_tail.rstrip unless output_tail.empty?
|
|
71
95
|
lines.join("\n")
|
|
@@ -14,6 +14,10 @@ module Samagotchi
|
|
|
14
14
|
def self.name = NAME
|
|
15
15
|
|
|
16
16
|
def self.call(content, path:)
|
|
17
|
+
# A native call without content/text arrives as nil; check before
|
|
18
|
+
# touching the file, or File.write(path, nil) would empty it.
|
|
19
|
+
return "Error: missing content" unless content.is_a?(String)
|
|
20
|
+
|
|
17
21
|
path = ToolPath.normalize(path)
|
|
18
22
|
FileUtils.mkdir_p(File.dirname(path))
|
|
19
23
|
File.write(path, content)
|
data/lib/samagotchi/turn_flow.rb
CHANGED
|
@@ -143,9 +143,19 @@ module Samagotchi
|
|
|
143
143
|
@checkpoint = nil
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
-
# A reminder turn
|
|
146
|
+
# A reminder turn is about to run: a pending continue offer goes, as for
|
|
147
|
+
# a new prompt (the partial turn stays), so a later "no" can't roll the
|
|
148
|
+
# reminder's exchange back with it.
|
|
149
|
+
# @return [Boolean] whether an offer was pending
|
|
150
|
+
def before_reminder_turn
|
|
151
|
+
offered = awaiting_continue?
|
|
152
|
+
@offer = nil
|
|
153
|
+
offered
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# A reminder turn ran: the rollback window closes.
|
|
147
157
|
def after_reminder_turn
|
|
148
|
-
@checkpoint = nil
|
|
158
|
+
@checkpoint = nil
|
|
149
159
|
end
|
|
150
160
|
|
|
151
161
|
private
|
data/lib/samagotchi/turn_note.rb
CHANGED
|
@@ -13,6 +13,10 @@ module Samagotchi
|
|
|
13
13
|
KIND = "turn_note"
|
|
14
14
|
OPEN = "[SYSTEM: "
|
|
15
15
|
CLOSE = "]"
|
|
16
|
+
FAILED = "the previous turn failed before any answer: "
|
|
17
|
+
RESTORED = "The message went back to the user, who may send it again."
|
|
18
|
+
TASKS_LISTED = 5
|
|
19
|
+
TASK_COMMAND_CHARS = 60
|
|
16
20
|
|
|
17
21
|
module_function
|
|
18
22
|
|
|
@@ -21,28 +25,48 @@ module Samagotchi
|
|
|
21
25
|
# failed user message is not in the conversation any more)
|
|
22
26
|
# @param continued [Boolean] it was a continue turn (no user message)
|
|
23
27
|
def failed(summary, restored: false, continued: false)
|
|
24
|
-
tail = if restored then
|
|
28
|
+
tail = if restored then RESTORED
|
|
25
29
|
elsif continued then "The continued turn stopped there."
|
|
26
30
|
else "The user's last message was not answered."
|
|
27
31
|
end
|
|
28
|
-
message("
|
|
32
|
+
message("#{FAILED}#{one_line(summary)}. #{tail}")
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
# @param reason [Symbol, String, nil] the cancel reason (:ctrl_c, :user…)
|
|
32
36
|
# @param seconds [Numeric, nil] how long the turn had run
|
|
33
37
|
# @param shown [Boolean] visible text had streamed (an `[interrupted]`
|
|
34
38
|
# model message precedes this note)
|
|
35
|
-
|
|
39
|
+
# @param running_tasks [Array<Hash>] {id:, command:} of this session's
|
|
40
|
+
# background tasks still running: a Stop doesn't end them
|
|
41
|
+
def cancelled(reason, seconds: nil, shown: false, running_tasks: [])
|
|
36
42
|
why = reason.to_s.empty? ? "" : " (#{reason.to_s.tr("_", "-")})"
|
|
37
43
|
after = seconds ? " after #{seconds.round}s" : ""
|
|
38
44
|
what = shown ? "the answer above ends where it was cut off." : "no answer had been shown."
|
|
39
|
-
message("the previous turn was cancelled#{why}#{after}; #{what}")
|
|
45
|
+
message("the previous turn was cancelled#{why}#{after}; #{what}#{still_running(running_tasks)}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def still_running(tasks)
|
|
49
|
+
return "" if tasks.empty?
|
|
50
|
+
|
|
51
|
+
listed = tasks.first(TASKS_LISTED).map do |task|
|
|
52
|
+
command = one_line(task[:command])
|
|
53
|
+
command = "#{command[0, TASK_COMMAND_CHARS - 1]}…" if command.length > TASK_COMMAND_CHARS
|
|
54
|
+
"task #{task[:id]} (#{command})"
|
|
55
|
+
end
|
|
56
|
+
listed << "#{tasks.size - TASKS_LISTED} more (task_list)" if tasks.size > TASKS_LISTED
|
|
57
|
+
" Still running: #{listed.join(", ")}. Continue with task_wait <id> or stop with task_stop <id>."
|
|
40
58
|
end
|
|
41
59
|
|
|
42
60
|
def empty
|
|
43
61
|
message("the previous turn ended with no visible answer (thinking only, or nothing). The user's last message is still unanswered.")
|
|
44
62
|
end
|
|
45
63
|
|
|
64
|
+
# The hidden nudge before a retry of an empty answer (EmptyAnswerRetry):
|
|
65
|
+
# the model never sees its empty generation, so it is told what happened.
|
|
66
|
+
def empty_retry
|
|
67
|
+
message("your last reply had no visible answer. Answer the user's last message now, briefly.")
|
|
68
|
+
end
|
|
69
|
+
|
|
46
70
|
def note?(entry)
|
|
47
71
|
return false unless entry.respond_to?(:[])
|
|
48
72
|
|
|
@@ -56,11 +80,41 @@ module Samagotchi
|
|
|
56
80
|
# +messages+ plus +note+, in place of a note already at the tail (behind
|
|
57
81
|
# context notes at most): failed retries leave one note, not a pile.
|
|
58
82
|
def replace_trailing(messages, note)
|
|
83
|
+
without_trailing(messages) << note
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A copy of +messages+ without the note at its tail (behind context
|
|
87
|
+
# notes at most).
|
|
88
|
+
def without_trailing(messages)
|
|
59
89
|
list = Array(messages).dup
|
|
90
|
+
index = trailing_index(list)
|
|
91
|
+
list.delete_at(index) if index
|
|
92
|
+
list
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The index of the note at the tail of +list+ (behind context notes at
|
|
96
|
+
# most), or nil.
|
|
97
|
+
def trailing_index(list)
|
|
60
98
|
index = list.length - 1
|
|
61
99
|
index -= 1 while index >= 0 && (list[index][:role] || list[index]["role"]).to_s == "system" && !note?(list[index])
|
|
62
|
-
|
|
63
|
-
|
|
100
|
+
index >= 0 && note?(list[index]) ? index : nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The failure summary of the note at the tail of +messages+ (behind
|
|
104
|
+
# context notes at most) when it says a failed turn's prompt went back
|
|
105
|
+
# to the user, else nil: the prompt is not in the conversation any more
|
|
106
|
+
# (the session's last_prompt holds it), so a UI shows it from here.
|
|
107
|
+
def restored_failure(messages)
|
|
108
|
+
list = Array(messages)
|
|
109
|
+
index = trailing_index(list)
|
|
110
|
+
return nil unless index
|
|
111
|
+
|
|
112
|
+
text = (list[index][:content] || list[index]["content"]).to_s
|
|
113
|
+
prefix = "#{OPEN}#{FAILED}"
|
|
114
|
+
suffix = ". #{RESTORED}#{CLOSE}"
|
|
115
|
+
return nil unless text.start_with?(prefix) && text.end_with?(suffix)
|
|
116
|
+
|
|
117
|
+
text[prefix.length...-suffix.length]
|
|
64
118
|
end
|
|
65
119
|
|
|
66
120
|
# The last message is a reply cut short (`[interrupted]`).
|