samagotchi 0.12.0 → 0.13.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 +62 -1
- data/README.md +20 -7
- data/bin/chi +5 -2
- data/docs/architecture.md +4 -2
- data/docs/cli.md +34 -10
- data/docs/configuration.md +29 -8
- data/docs/desktop.md +10 -6
- data/docs/guardrails.md +73 -7
- data/docs/hooks.md +13 -9
- data/docs/memory.md +5 -3
- data/docs/plugins.md +4 -4
- data/docs/sessions.md +9 -6
- data/docs/sub-agent.md +128 -0
- data/lib/samagotchi/answer_command.rb +220 -0
- data/lib/samagotchi/bridge.rb +11 -1
- data/lib/samagotchi/bridge_client.rb +8 -3
- data/lib/samagotchi/bundle_command.rb +18 -10
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +19 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +5 -5
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cli/command.rb +3 -2
- data/lib/samagotchi/cli/exit.rb +23 -0
- data/lib/samagotchi/cli/parent_wait.rb +82 -0
- data/lib/samagotchi/config.rb +50 -18
- data/lib/samagotchi/desktop_command.rb +5 -3
- data/lib/samagotchi/engine.rb +40 -14
- data/lib/samagotchi/guardrails/parent_approvals.rb +155 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +10 -3
- data/lib/samagotchi/host_registry.rb +0 -1
- data/lib/samagotchi/llm/openai_chat.rb +9 -1
- data/lib/samagotchi/memory_bundle/installer.rb +12 -0
- data/lib/samagotchi/memory_bundle/status.rb +9 -1
- data/lib/samagotchi/memory_paths.rb +8 -0
- data/lib/samagotchi/model_profile.rb +30 -16
- data/lib/samagotchi/parent_report.rb +213 -0
- data/lib/samagotchi/plugin/api.rb +15 -2
- data/lib/samagotchi/prompt_literal_guard.rb +4 -2
- data/lib/samagotchi/question_desk.rb +29 -2
- data/lib/samagotchi/recap_setup.rb +15 -5
- data/lib/samagotchi/reply_wait.rb +5 -1
- data/lib/samagotchi/send_command.rb +50 -75
- data/lib/samagotchi/session.rb +15 -0
- data/lib/samagotchi/session_manager.rb +34 -7
- data/lib/samagotchi/sessions_command.rb +13 -3
- data/lib/samagotchi/system_prompt.rb +10 -2
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +33 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -5
- data/lib/samagotchi/terminal_ui.rb +15 -1
- data/lib/samagotchi/thought_stream_splitter.rb +4 -4
- data/lib/samagotchi/tool_call_parser.rb +5 -6
- data/lib/samagotchi/tools/builtin_calls.rb +14 -3
- data/lib/samagotchi/tools/builtins.rb +14 -2
- data/lib/samagotchi/tools/delegate_wait.rb +16 -4
- data/lib/samagotchi/tools/execute.rb +8 -8
- data/lib/samagotchi/tools/task_create.rb +3 -2
- data/lib/samagotchi/tools/task_runtime.rb +7 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +4 -3
- data/lib/samagotchi/web/message_parts.rb +2 -1
- data/lib/samagotchi/web/server.rb +2 -1
- data/lib/samagotchi/web/session_summary.rb +3 -11
- metadata +7 -1
|
@@ -15,6 +15,7 @@ require_relative "host_registry"
|
|
|
15
15
|
require_relative "session"
|
|
16
16
|
require_relative "owner_lock"
|
|
17
17
|
require_relative "engine"
|
|
18
|
+
require_relative "guardrails/parent_approvals"
|
|
18
19
|
require_relative "tools/memory"
|
|
19
20
|
require_relative "output_formatter"
|
|
20
21
|
require_relative "turn_flow"
|
|
@@ -1138,9 +1139,16 @@ module Samagotchi
|
|
|
1138
1139
|
end
|
|
1139
1140
|
|
|
1140
1141
|
begin
|
|
1141
|
-
|
|
1142
|
+
parent = parent_answers? ? { client_id: Guardrails::ParentApprovals::CLIENT_ID } : {}
|
|
1143
|
+
@engine.answer_question(id: prompt.id, selected: answer.selected, freeform: answer.freeform, **parent)
|
|
1142
1144
|
close_question_widget(prompt, prompt.answer_text(answer))
|
|
1143
1145
|
return true
|
|
1146
|
+
rescue QuestionDesk::Refused => e
|
|
1147
|
+
# A parent's allow guardrails.parent_approvals doesn't let through:
|
|
1148
|
+
# the question stays open for a deny (or the end of the input).
|
|
1149
|
+
@surface.commit("#{paint(QUESTION_PROMPT, 33)}#{raw}")
|
|
1150
|
+
@surface.commit(Guardrails::ParentApprovals.message(e.reason, typed: true))
|
|
1151
|
+
next
|
|
1144
1152
|
rescue ArgumentError => e
|
|
1145
1153
|
@surface.commit("Invalid: #{e.message}. Try again.")
|
|
1146
1154
|
next
|
|
@@ -1151,6 +1159,12 @@ module Samagotchi
|
|
|
1151
1159
|
end
|
|
1152
1160
|
end
|
|
1153
1161
|
|
|
1162
|
+
# Whether the answers typed here are a parent agent's (piped stdin, an
|
|
1163
|
+
# agent marker): QuestionDesk holds them to guardrails.parent_approvals.
|
|
1164
|
+
def parent_answers?
|
|
1165
|
+
Guardrails::ParentApprovals.parent_process?
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1154
1168
|
def close_question_widget(prompt, outcome)
|
|
1155
1169
|
@surface.clear_slot(:notes)
|
|
1156
1170
|
@surface.commit(prompt.summary(outcome, paint: method(:paint)))
|
|
@@ -25,15 +25,15 @@ module Samagotchi
|
|
|
25
25
|
# not a block and stays in `:text` (the model doesn't emit it; the system
|
|
26
26
|
# prompt carries it).
|
|
27
27
|
class ThoughtStreamSplitter
|
|
28
|
-
# Gemma 4's thought channel (ToolCallParser::Gemma strips the same pair).
|
|
29
|
-
GEMMA_THOUGHT_CHANNEL = { open: "<|channel>thought", close: "<channel|>" }.freeze
|
|
30
|
-
|
|
31
28
|
# @return [Array<Hash>] each entry { open: String, close: String, lane: :thinking|:drop }
|
|
32
29
|
def self.for_profile(profile)
|
|
33
30
|
blocks = []
|
|
34
31
|
# A thinking block needs a close marker to be bounded.
|
|
35
32
|
blocks << { open: profile.thought_open, close: profile.thought_close, lane: :thinking } if profile.thought_close
|
|
36
|
-
|
|
33
|
+
# Gemma 4's thought channel (ToolCallParser::Gemma strips the same pair).
|
|
34
|
+
if profile.thought_channel_open
|
|
35
|
+
blocks << { open: profile.thought_channel_open, close: profile.thought_channel_close, lane: :thinking }
|
|
36
|
+
end
|
|
37
37
|
blocks << { open: profile.tool_call_open, close: profile.tool_call_close, lane: :drop }
|
|
38
38
|
new(blocks)
|
|
39
39
|
end
|
|
@@ -38,8 +38,8 @@ module Samagotchi
|
|
|
38
38
|
# Digits after the first character: plugin tools may have them
|
|
39
39
|
# (Plugin::Api::TOOL_NAME).
|
|
40
40
|
TOOL_CALL_BODY_RE = /\Acall:([a-z_][a-z0-9_]{0,49})\{/
|
|
41
|
-
THOUGHT_CHANNEL_OPEN =
|
|
42
|
-
THOUGHT_CHANNEL_CLOSE =
|
|
41
|
+
THOUGHT_CHANNEL_OPEN = ModelProfile::GEMMA_THOUGHT_CHANNEL_OPEN
|
|
42
|
+
THOUGHT_CHANNEL_CLOSE = ModelProfile::GEMMA_THOUGHT_CHANNEL_CLOSE
|
|
43
43
|
CONTROL_TOKEN_START = "<|"
|
|
44
44
|
|
|
45
45
|
def initialize(profile)
|
|
@@ -305,10 +305,10 @@ module Samagotchi
|
|
|
305
305
|
end
|
|
306
306
|
|
|
307
307
|
def parse(text)
|
|
308
|
-
read(text).map { |call| Tools::BuiltinCalls.build(call[:name], call[:args]
|
|
308
|
+
read(text).map { |call| Tools::BuiltinCalls.build(call[:name], call[:args]) }
|
|
309
309
|
end
|
|
310
310
|
|
|
311
|
-
# Each <tool_call> block as {name:, args
|
|
311
|
+
# Each <tool_call> block as {name:, args:}: its <parameter=…>
|
|
312
312
|
# (or <arg_key>/<arg_value>) text by lowercased key, unbuilt.
|
|
313
313
|
def read(text)
|
|
314
314
|
results = []
|
|
@@ -324,8 +324,7 @@ module Samagotchi
|
|
|
324
324
|
|
|
325
325
|
body = text[body_start...close_pos]
|
|
326
326
|
if (name = qwen_function_name(body))
|
|
327
|
-
|
|
328
|
-
results << { name: name, args: params, raw: params.to_s }
|
|
327
|
+
results << { name: name, args: qwen_params(body) }
|
|
329
328
|
end
|
|
330
329
|
pos = close_pos + tool_close.length
|
|
331
330
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
3
4
|
require_relative "../tool_declarations"
|
|
4
5
|
require_relative "ask_user_question"
|
|
5
6
|
|
|
@@ -18,7 +19,7 @@ module Samagotchi
|
|
|
18
19
|
# and the handler reading it.
|
|
19
20
|
#
|
|
20
21
|
# A tool that is not built in keeps its arguments whole on args: (typed
|
|
21
|
-
# by its schema at dispatch, Tools::Args) with
|
|
22
|
+
# by its schema at dispatch, Tools::Args) with their JSON as its content.
|
|
22
23
|
module BuiltinCalls
|
|
23
24
|
# Per tool, what the schema alone doesn't say:
|
|
24
25
|
# content: the property (or ordered list of keys to try) that fills
|
|
@@ -103,7 +104,7 @@ module Samagotchi
|
|
|
103
104
|
# @param name [String]
|
|
104
105
|
# @param args [Hash] string keys, as the model gave them
|
|
105
106
|
# @param raw [String, nil] the call's text, the content of a tool
|
|
106
|
-
# that isn't built in
|
|
107
|
+
# that isn't built in when no argument could be read from it
|
|
107
108
|
# @return [Hash]
|
|
108
109
|
def build(name, args, raw: nil)
|
|
109
110
|
name = name.to_s
|
|
@@ -139,7 +140,17 @@ module Samagotchi
|
|
|
139
140
|
end
|
|
140
141
|
|
|
141
142
|
def passthrough(name, args, raw)
|
|
142
|
-
{ name: name, content:
|
|
143
|
+
{ name: name, content: passthrough_content(args, raw), path: nil, scope: nil, args: args }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The arguments as JSON, one form for every model format and Ruby
|
|
147
|
+
# version; the raw text when none could be read from it.
|
|
148
|
+
def passthrough_content(args, raw)
|
|
149
|
+
return raw.to_s if args.empty? && !raw.to_s.empty?
|
|
150
|
+
|
|
151
|
+
JSON.generate(args)
|
|
152
|
+
rescue JSON::GeneratorError
|
|
153
|
+
args.values.join(" ")
|
|
143
154
|
end
|
|
144
155
|
end
|
|
145
156
|
end
|
|
@@ -21,6 +21,7 @@ require_relative "send_note"
|
|
|
21
21
|
require_relative "delegate"
|
|
22
22
|
require_relative "delegate_result"
|
|
23
23
|
require_relative "ask_user_question"
|
|
24
|
+
require_relative "../guardrails/parent_approvals"
|
|
24
25
|
|
|
25
26
|
module Samagotchi
|
|
26
27
|
module Tools
|
|
@@ -72,13 +73,15 @@ module Samagotchi
|
|
|
72
73
|
TaskCreate::NAME => lambda do |call, kctx|
|
|
73
74
|
next Execute::NOT_RUN_ON_STOP if cancelled_proc(kctx).call
|
|
74
75
|
|
|
75
|
-
TaskCreate.call(call[:content], cwd: call[:cwd], env: call[:env])
|
|
76
|
+
TaskCreate.call(call[:content], cwd: call[:cwd], env: call[:env], marker_env: parent_env(kctx))
|
|
76
77
|
end,
|
|
77
78
|
TaskWait::NAME => lambda do |call, kctx|
|
|
78
79
|
TaskWait.call(call[:content], timeout: call[:timeout], tail_lines: call[:tail_lines],
|
|
79
80
|
done_pattern: call[:done_pattern], cancelled: cancelled_proc(kctx))
|
|
80
81
|
end,
|
|
81
|
-
Execute::NAME =>
|
|
82
|
+
Execute::NAME => lambda do |call, kctx|
|
|
83
|
+
Execute.call(call[:content], cwd: call[:cwd], cancelled: cancelled_proc(kctx), env: parent_env(kctx))
|
|
84
|
+
end,
|
|
82
85
|
RegisterReminder::NAME => lambda do |call, kctx|
|
|
83
86
|
RegisterReminder.call(call[:content], reminder_store: kctx.reminder_store, description: call[:description],
|
|
84
87
|
interval_minutes: call[:interval_minutes])
|
|
@@ -99,6 +102,15 @@ module Samagotchi
|
|
|
99
102
|
|
|
100
103
|
module_function
|
|
101
104
|
|
|
105
|
+
# What execute and task_create export into the commands they run: a
|
|
106
|
+
# chi started there answers as a parent agent
|
|
107
|
+
# (Guardrails::ParentApprovals.parent_process?), even under a PTY
|
|
108
|
+
# wrapper. The session's id, "chi" without one.
|
|
109
|
+
def parent_env(kctx)
|
|
110
|
+
id = kctx.peers.respond_to?(:session_id) ? kctx.peers.session_id.to_s : ""
|
|
111
|
+
{ Guardrails::ParentApprovals::PARENT_SESSION_ENV => id.empty? ? "chi" : id }
|
|
112
|
+
end
|
|
113
|
+
|
|
102
114
|
# Stop flips the turn's controller, seen through the Engine's PeerView
|
|
103
115
|
# (as DelegateWait does); a bare kernel has no peers: never cancelled.
|
|
104
116
|
def cancelled_proc(kctx)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../session"
|
|
4
4
|
require_relative "../reply_wait"
|
|
5
|
+
require_relative "../parent_report"
|
|
5
6
|
require_relative "output_guardrails"
|
|
6
7
|
require_relative "peers"
|
|
7
8
|
|
|
@@ -18,6 +19,10 @@ module Samagotchi
|
|
|
18
19
|
module DelegateWait
|
|
19
20
|
TIMEOUT_DEFAULT = 600
|
|
20
21
|
POLL_INTERVAL = 0.5
|
|
22
|
+
# Seconds with no worker before the child counts as gone: as long as
|
|
23
|
+
# Delegate gives a starting child (its worker takes the owner lock
|
|
24
|
+
# only once it has booted).
|
|
25
|
+
OWNER_GRACE = 15
|
|
21
26
|
# Cut like an execute result: full up to TRUNCATE_AT bytes, else a
|
|
22
27
|
# head-and-tail preview of PREVIEW bytes.
|
|
23
28
|
TRUNCATE_AT_BYTES = OutputGuardrails::DEFAULT_TRUNCATE_AT_BYTES
|
|
@@ -39,13 +44,16 @@ module Samagotchi
|
|
|
39
44
|
# @param peers [Peers] the parent (session_id, state_dir, cancelled?)
|
|
40
45
|
# @param timeout [Integer] seconds
|
|
41
46
|
# @param poll_interval [Float]
|
|
47
|
+
# @param owner_grace [Numeric] seconds with no live worker before
|
|
48
|
+
# the child's worker counts as gone
|
|
42
49
|
# @return [String] the tool result
|
|
43
|
-
def call(child_id, peers:, timeout: TIMEOUT_DEFAULT, poll_interval: POLL_INTERVAL)
|
|
50
|
+
def call(child_id, peers:, timeout: TIMEOUT_DEFAULT, poll_interval: POLL_INTERVAL, owner_grace: OWNER_GRACE)
|
|
44
51
|
sd = peers.state_dir || Session.default_state_dir
|
|
45
52
|
key = [peers.session_id, child_id]
|
|
46
53
|
cancelled = -> { peers.cancelled? }
|
|
47
54
|
wait = ReplyWait.call(child_id, state_dir: sd, cursor: seen[key], timeout: timeout.to_i,
|
|
48
|
-
poll_interval: poll_interval, cancelled: cancelled, baseline: baselines[key]
|
|
55
|
+
poll_interval: poll_interval, cancelled: cancelled, baseline: baselines[key],
|
|
56
|
+
owner_grace: owner_grace)
|
|
49
57
|
# The turn sent to is handed over: a later wait looks for a later one.
|
|
50
58
|
baselines.delete(key) if %i[done no_reply].include?(wait.status)
|
|
51
59
|
case wait.status
|
|
@@ -62,6 +70,9 @@ module Samagotchi
|
|
|
62
70
|
result(child_id, "canceled", "wait canceled; the child keeps running; delegate_result #{child_id} waits again")
|
|
63
71
|
when :no_reply
|
|
64
72
|
result(child_id, "no_reply", "the child's turn ended without a reply (canceled, failed or empty); its session shows what happened")
|
|
73
|
+
when :worker_gone
|
|
74
|
+
result(child_id, "worker_gone",
|
|
75
|
+
"the child's worker is gone (it stopped or crashed); delegate with session: #{child_id} starts it again with a message")
|
|
65
76
|
else
|
|
66
77
|
timeout_result(child_id, timeout)
|
|
67
78
|
end
|
|
@@ -98,9 +109,10 @@ module Samagotchi
|
|
|
98
109
|
"chi --attach #{child_id} shows it.")
|
|
99
110
|
end
|
|
100
111
|
|
|
112
|
+
# The whole question, as `chi send --wait` prints it (ParentReport):
|
|
113
|
+
# its text, options and the commands that answer it.
|
|
101
114
|
def waiting_text(child_id, pending)
|
|
102
|
-
|
|
103
|
-
"Child #{child_id} is waiting for an answer (#{what}); attach with chi --attach #{child_id} or answer it in the web. " \
|
|
115
|
+
"Child #{child_id} is #{ParentReport.question_text(pending, session_id: child_id)}" \
|
|
104
116
|
"delegate_result #{child_id} waits again once it is answered."
|
|
105
117
|
end
|
|
106
118
|
|
|
@@ -25,7 +25,8 @@ module Samagotchi
|
|
|
25
25
|
|
|
26
26
|
# @param cancelled [#call] true once the user stopped the turn: the
|
|
27
27
|
# command is killed (or not started) and its output so far returned
|
|
28
|
-
|
|
28
|
+
# @param env [Hash, nil] added to the command's environment
|
|
29
|
+
def self.call(command, cwd: nil, cancelled: -> { false }, env: nil)
|
|
29
30
|
return NOT_RUN_ON_STOP if cancelled.call
|
|
30
31
|
|
|
31
32
|
command = command.strip
|
|
@@ -33,7 +34,8 @@ module Samagotchi
|
|
|
33
34
|
return "Error: cwd not found: #{resolved_cwd}" unless resolved_cwd
|
|
34
35
|
|
|
35
36
|
timeout_sec = timeout_seconds
|
|
36
|
-
stdout, stderr, status = run_command(command, timeout_sec: timeout_sec, cwd: resolved_cwd, cancelled: cancelled
|
|
37
|
+
stdout, stderr, status = run_command(command, timeout_sec: timeout_sec, cwd: resolved_cwd, cancelled: cancelled,
|
|
38
|
+
env: env)
|
|
37
39
|
|
|
38
40
|
stdout_block = output_block("stdout", stdout)
|
|
39
41
|
stderr_block = output_block("stderr", stderr)
|
|
@@ -66,7 +68,7 @@ module Samagotchi
|
|
|
66
68
|
end
|
|
67
69
|
private_class_method :stopped_result
|
|
68
70
|
|
|
69
|
-
def self.run_command(command, timeout_sec:, cwd:, cancelled: -> { false })
|
|
71
|
+
def self.run_command(command, timeout_sec:, cwd:, cancelled: -> { false }, env: nil)
|
|
70
72
|
stdout_text = ""
|
|
71
73
|
stderr_text = ""
|
|
72
74
|
status = nil
|
|
@@ -75,7 +77,7 @@ module Samagotchi
|
|
|
75
77
|
started = monotonic_time
|
|
76
78
|
deadline = started + timeout_sec
|
|
77
79
|
|
|
78
|
-
Open3.popen3(command, chdir: cwd, pgroup: true) do |stdin, stdout, stderr, wait_thr|
|
|
80
|
+
Open3.popen3(env || {}, command, chdir: cwd, pgroup: true) do |stdin, stdout, stderr, wait_thr|
|
|
79
81
|
stdin.close
|
|
80
82
|
stdout_reader = reader_thread_for(stdout)
|
|
81
83
|
stderr_reader = reader_thread_for(stderr)
|
|
@@ -173,11 +175,9 @@ module Samagotchi
|
|
|
173
175
|
end
|
|
174
176
|
private_class_method :monotonic_time
|
|
175
177
|
|
|
178
|
+
# execute.timeout_sec (SAMAGOTCHI_EXECUTE_TIMEOUT_SEC); 0 or less = the default.
|
|
176
179
|
def self.timeout_seconds
|
|
177
|
-
|
|
178
|
-
return TIMEOUT_SEC if value.nil? || value <= 0
|
|
179
|
-
|
|
180
|
-
value
|
|
180
|
+
OutputGuardrails.config_positive_int("execute.timeout_sec", TIMEOUT_SEC)
|
|
181
181
|
end
|
|
182
182
|
private_class_method :timeout_seconds
|
|
183
183
|
|
|
@@ -9,8 +9,9 @@ module Samagotchi
|
|
|
9
9
|
|
|
10
10
|
def self.name = NAME
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
# @param marker_env [Hash] set after the model's env (Builtins.parent_env)
|
|
13
|
+
def self.call(command, cwd: nil, env: nil, marker_env: {})
|
|
14
|
+
record, error = TaskRuntime.create_task(command, cwd: cwd, env: env, marker_env: marker_env)
|
|
14
15
|
return error if error
|
|
15
16
|
|
|
16
17
|
[
|
|
@@ -19,10 +19,13 @@ module Samagotchi
|
|
|
19
19
|
METADATA_VERSION = 1
|
|
20
20
|
OUTPUT_TAIL_READ_BYTES = 16 * 1024
|
|
21
21
|
SANITIZED_ENV_KEYS = %w[RUBYOPT RUBYLIB BUNDLE_GEMFILE BUNDLE_BIN_PATH BUNDLER_VERSION].freeze
|
|
22
|
+
# Set by chi (Builtins.parent_env); the model's env can't change it.
|
|
23
|
+
MARKER_ENV_KEYS = %w[SAMAGOTCHI_PARENT_SESSION].freeze
|
|
22
24
|
|
|
23
25
|
module_function
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
# @param marker_env [Hash] chi's own variables, set over the model's +env+
|
|
28
|
+
def create_task(command, cwd: nil, env: nil, marker_env: {})
|
|
26
29
|
normalized_command = command.to_s.strip
|
|
27
30
|
return [nil, "Error: command is required"] if normalized_command.empty?
|
|
28
31
|
|
|
@@ -32,6 +35,8 @@ module Samagotchi
|
|
|
32
35
|
spawn_env, env_error = spawn_env_for(env)
|
|
33
36
|
return [nil, env_error] if env_error
|
|
34
37
|
|
|
38
|
+
spawn_env = spawn_env.merge(marker_env || {})
|
|
39
|
+
|
|
35
40
|
task_id = generate_task_id
|
|
36
41
|
task_dir = task_dir_for(task_id)
|
|
37
42
|
output_path = File.join(task_dir, OUTPUT_FILENAME)
|
|
@@ -258,7 +263,7 @@ module Samagotchi
|
|
|
258
263
|
overrides.each do |key, value|
|
|
259
264
|
return [nil, "Error: env keys and values must be strings"] unless key.is_a?(String) && value.is_a?(String)
|
|
260
265
|
return [nil, "Error: env keys and values cannot contain NUL bytes"] if key.include?("\0") || value.include?("\0")
|
|
261
|
-
return [nil, "Error: env key is reserved: #{key}"] if SANITIZED_ENV_KEYS.include?(key)
|
|
266
|
+
return [nil, "Error: env key is reserved: #{key}"] if SANITIZED_ENV_KEYS.include?(key) || MARKER_ENV_KEYS.include?(key)
|
|
262
267
|
|
|
263
268
|
normalized[key] = value
|
|
264
269
|
end
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -539,11 +539,12 @@ module Samagotchi
|
|
|
539
539
|
last_event_seq = snapshot ? snapshot["event_seq"] : bridge_event_seq(id)
|
|
540
540
|
current_turn = turn_snapshot && turn_snapshot["current_turn"]
|
|
541
541
|
owner = session_owner(id)
|
|
542
|
-
# The file's question only
|
|
543
|
-
# saved can't be answered (its turn is gone)
|
|
542
|
+
# The file's question only while a worker owns the session: one a
|
|
543
|
+
# dead worker saved can't be answered (its turn is gone), nor a chi
|
|
544
|
+
# REPL's (it shares nothing with the web).
|
|
544
545
|
pending = if turn_snapshot
|
|
545
546
|
current_turn && current_turn["pending_question"]
|
|
546
|
-
elsif owner
|
|
547
|
+
elsif owner&.worker?
|
|
547
548
|
session.pending_question
|
|
548
549
|
end
|
|
549
550
|
# A /model in the worker changes it before the file catches up.
|
|
@@ -46,7 +46,8 @@ module Samagotchi
|
|
|
46
46
|
|
|
47
47
|
QWEN_THINK = %r{<think>(.*?)</think>}m
|
|
48
48
|
QWEN_OPEN_THINK = %r{\A(.*?)</think>}m # the template opened the block
|
|
49
|
-
GEMMA_THOUGHT =
|
|
49
|
+
GEMMA_THOUGHT = Regexp.new("#{Regexp.escape(ModelProfile::GEMMA_THOUGHT_CHANNEL_OPEN)}(.*?)" \
|
|
50
|
+
"(?:#{Regexp.escape(ModelProfile::GEMMA_THOUGHT_CHANNEL_CLOSE)}|\\z)", Regexp::MULTILINE)
|
|
50
51
|
|
|
51
52
|
CallRef = Struct.new(:name, :arguments)
|
|
52
53
|
|
|
@@ -16,6 +16,7 @@ require_relative "../config"
|
|
|
16
16
|
require_relative "../project_scope"
|
|
17
17
|
require_relative "../log"
|
|
18
18
|
require_relative "../version"
|
|
19
|
+
require_relative "../cli/exit"
|
|
19
20
|
|
|
20
21
|
module Samagotchi
|
|
21
22
|
module Web
|
|
@@ -99,7 +100,7 @@ module Samagotchi
|
|
|
99
100
|
# Ctrl-C: the server has stopped (start's ensure); one line, and the
|
|
100
101
|
# status a shell gives a command it interrupted.
|
|
101
102
|
puts "#{"\n" if $stdout.tty?}Chi Web stopped."
|
|
102
|
-
|
|
103
|
+
CLI::Exit::INTERRUPTED
|
|
103
104
|
end
|
|
104
105
|
|
|
105
106
|
# @param hub [SessionHub, nil] the session projection the page streams
|
|
@@ -61,22 +61,14 @@ module Samagotchi
|
|
|
61
61
|
bridge_up: up,
|
|
62
62
|
# For the tab's notifications (notify.js): an open question, the
|
|
63
63
|
# running turn's open card with actions, and how the last turn
|
|
64
|
-
# ended. A question saved by a worker that died is not open
|
|
65
|
-
|
|
64
|
+
# ended. A question saved by a worker that died is not open, nor a
|
|
65
|
+
# chi REPL's (SessionManager.worker_live?'s rule: only a worker shares it).
|
|
66
|
+
pending_question: session.waiting_question(live: !!owner&.worker?),
|
|
66
67
|
pending_card: up ? Bridge::PendingCard.read(session_dir) : nil,
|
|
67
68
|
last_turn: session.last_turn
|
|
68
69
|
}
|
|
69
70
|
end
|
|
70
71
|
|
|
71
|
-
# The open question as {id:, kind:} ("approval" for a guardrail's,
|
|
72
|
-
# else "question"); nil when there is none. The text stays out.
|
|
73
|
-
def pending_question_for(session)
|
|
74
|
-
pending = session.pending_question
|
|
75
|
-
return nil unless pending.is_a?(Hash) && pending[:id]
|
|
76
|
-
|
|
77
|
-
{ id: pending[:id], kind: pending[:kind].to_s == "approval" ? "approval" : "question" }
|
|
78
|
-
end
|
|
79
|
-
|
|
80
72
|
# status is turn state (idle/running). The live worker's snapshot is the
|
|
81
73
|
# truth; on disk, a "running" with no live owner was left by a worker
|
|
82
74
|
# that died mid-turn.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -136,7 +136,9 @@ files:
|
|
|
136
136
|
- docs/plugins.md
|
|
137
137
|
- docs/releasing.md
|
|
138
138
|
- docs/sessions.md
|
|
139
|
+
- docs/sub-agent.md
|
|
139
140
|
- lib/samagotchi.rb
|
|
141
|
+
- lib/samagotchi/answer_command.rb
|
|
140
142
|
- lib/samagotchi/answer_display.rb
|
|
141
143
|
- lib/samagotchi/archive_store.rb
|
|
142
144
|
- lib/samagotchi/atomic_file.rb
|
|
@@ -188,7 +190,9 @@ files:
|
|
|
188
190
|
- lib/samagotchi/bundles/system/self_map.md
|
|
189
191
|
- lib/samagotchi/cancellation_controller.rb
|
|
190
192
|
- lib/samagotchi/cli/command.rb
|
|
193
|
+
- lib/samagotchi/cli/exit.rb
|
|
191
194
|
- lib/samagotchi/cli/flags.rb
|
|
195
|
+
- lib/samagotchi/cli/parent_wait.rb
|
|
192
196
|
- lib/samagotchi/client.rb
|
|
193
197
|
- lib/samagotchi/commands/registry.rb
|
|
194
198
|
- lib/samagotchi/config.rb
|
|
@@ -224,6 +228,7 @@ files:
|
|
|
224
228
|
- lib/samagotchi/guardrails/gate.rb
|
|
225
229
|
- lib/samagotchi/guardrails/load_failures.rb
|
|
226
230
|
- lib/samagotchi/guardrails/model_size.rb
|
|
231
|
+
- lib/samagotchi/guardrails/parent_approvals.rb
|
|
227
232
|
- lib/samagotchi/guardrails/protected_paths.rb
|
|
228
233
|
- lib/samagotchi/guardrails/rules.rb
|
|
229
234
|
- lib/samagotchi/guardrails/scratch_writes.rb
|
|
@@ -286,6 +291,7 @@ files:
|
|
|
286
291
|
- lib/samagotchi/note_command.rb
|
|
287
292
|
- lib/samagotchi/output_formatter.rb
|
|
288
293
|
- lib/samagotchi/owner_lock.rb
|
|
294
|
+
- lib/samagotchi/parent_report.rb
|
|
289
295
|
- lib/samagotchi/paths.rb
|
|
290
296
|
- lib/samagotchi/pending_input_queue.rb
|
|
291
297
|
- lib/samagotchi/plugin/api.rb
|