samagotchi 0.86.0 → 0.87.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.
@@ -18,24 +18,26 @@ module Samagotchi
18
18
  # +toolkit+ a Toolkit or nil (SessionManager.spawn_session gives the
19
19
  # session config's toolkit.default then), +bundle_settings+ the bundle
20
20
  # settings chi --set gave ({bundle => {key => value}}, frozen; {} with
21
- # none), which Plugin::Context#settings lays over config.yml's.
21
+ # none), which Plugin::Context#settings lays over config.yml's,
22
+ # +parent_answers+ the session's own guardrails.parent_answers (a level
23
+ # String, tighten-only: Guardrails::ParentPolicy.effective) or nil.
22
24
  #
23
25
  # It doesn't save itself: the session keeps each setting in its own field
24
26
  # and file key, and resolves it on its own. A delegate child starts
25
- # without one (but the toolkit its delegate call named); a continue and a
26
- # fork copy their session's (.of).
27
+ # without one (but the toolkit its delegate call named, and its parent's
28
+ # parent_answers); a continue and a fork copy their session's (.of).
27
29
  SessionSetup = Data.define(:llm_context, :thinking, :reasoning_history, :appended_prompt_files, :toolkit,
28
- :bundle_settings) do
30
+ :bundle_settings, :parent_answers) do
29
31
  def initialize(llm_context: nil, thinking: nil, reasoning_history: nil, appended_prompt_files: [], toolkit: nil,
30
- bundle_settings: {})
32
+ bundle_settings: {}, parent_answers: nil)
31
33
  super(llm_context: llm_context, thinking: thinking, reasoning_history: reasoning_history,
32
34
  appended_prompt_files: Array(appended_prompt_files).map(&:to_s).freeze, toolkit: toolkit,
33
- bundle_settings: BundleSessionSettings.normalize(bundle_settings))
35
+ bundle_settings: BundleSessionSettings.normalize(bundle_settings), parent_answers: parent_answers&.to_s)
34
36
  end
35
37
 
36
38
  def empty?
37
39
  llm_context.nil? && thinking.nil? && reasoning_history.nil? && appended_prompt_files.empty? && toolkit.nil? &&
38
- bundle_settings.empty?
40
+ bundle_settings.empty? && parent_answers.nil?
39
41
  end
40
42
 
41
43
  # A session's own setup, for a session that starts from it. Its
@@ -48,7 +50,7 @@ module Samagotchi
48
50
 
49
51
  new(llm_context: session.llm_context, thinking: session.thinking, reasoning_history: session.reasoning_history,
50
52
  appended_prompt_files: session.appended_prompt_files, toolkit: Toolkit.of(session.toolkit),
51
- bundle_settings: session.bundle_settings)
53
+ bundle_settings: session.bundle_settings, parent_answers: session.parent_answers)
52
54
  end
53
55
  end
54
56
  end
@@ -53,6 +53,10 @@ module Samagotchi
53
53
  # the line an existing session's run prints (--set too)
54
54
  # @param bundle_settings [Hash] --set's values (BundleSessionSettings):
55
55
  # a new session's setup carries them; an existing session keeps its own
56
+ # @param parent_answers [String, nil] --parent-answers: a new session's
57
+ # own guardrails.parent_answers; an existing session keeps its own
58
+ # (never sent as a /parent-answers command: that would be a
59
+ # loosening route from a command line)
56
60
  # @param state_dir [String, nil] where the sessions are (the default
57
61
  # state dir by default): the worker's, for the attached loop too
58
62
  # @return [Symbol] :detached, :closed when the worker went away,
@@ -60,12 +64,12 @@ module Samagotchi
60
64
  # pipe) :turn_failed / :empty_answer / :unanswered (AttachedLoop#run)
61
65
  def run(attach: nil, shared: false, resume: nil, prompt: nil, model: nil, no_interrupt: false, default_input: true,
62
66
  memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], toolkit: nil,
63
- toolkit_flags: [], bundle_settings: {}, state_dir: nil)
67
+ toolkit_flags: [], bundle_settings: {}, parent_answers: nil, state_dir: nil)
64
68
  state_dir ||= Session.default_state_dir
65
69
  client = connect(attach: attach, shared: shared, resume: resume, model: model, state_dir: state_dir,
66
70
  memories: memories, muted_memories: muted_memories, llm_context: llm_context, thinking: thinking,
67
71
  appended_prompt_files: appended_prompt_files, toolkit: toolkit, toolkit_flags: toolkit_flags,
68
- bundle_settings: bundle_settings)
72
+ bundle_settings: bundle_settings, parent_answers: parent_answers)
69
73
  commands = []
70
74
  commands << "/model #{model}" if model && (attach || resume)
71
75
  commands << LLMContextOverride.command_line(llm_context) if (attach || resume) && !llm_context.empty?
@@ -83,7 +87,9 @@ module Samagotchi
83
87
  ensure
84
88
  close_surface(surface)
85
89
  end
86
- report_unanswered(attached.unanswered, session_id: client.session_id) if ended == :unanswered && attached.unanswered
90
+ if ended == :unanswered && attached.unanswered
91
+ report_unanswered(attached.unanswered, session_id: client.session_id, state_dir: state_dir)
92
+ end
87
93
  report_ended(ended)
88
94
  ended
89
95
  end
@@ -92,9 +98,12 @@ module Samagotchi
92
98
  # --wait` prints it (ParentReport), so a script or a parent agent
93
99
  # can answer it with chi answer.
94
100
  # @param pending [Hash] the pending question as the Bridge sent it
95
- def report_unanswered(pending, session_id:, err: $stderr)
101
+ # @param state_dir [String, nil] where the session file is: what a
102
+ # parent may pick follows its own parent_answers too
103
+ def report_unanswered(pending, session_id:, err: $stderr, state_dir: nil)
96
104
  question = PendingQuestion.from_file(pending)
97
- err.print("chi: #{ParentReport.question_text(question, session_id: session_id)}")
105
+ strategy = ParentReport.strategy_for(session_id, state_dir: state_dir || Session.default_state_dir)
106
+ err.print("chi: #{ParentReport.question_text(question, session_id: session_id, strategy: strategy)}")
98
107
  err.flush
99
108
  end
100
109
 
@@ -134,7 +143,7 @@ module Samagotchi
134
143
  # @raise [Error]
135
144
  def connect(attach: nil, shared: false, resume: nil, model: nil, state_dir: nil, wait: BRIDGE_WAIT,
136
145
  memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], toolkit: nil,
137
- toolkit_flags: [], bundle_settings: {}, err: $stderr)
146
+ toolkit_flags: [], bundle_settings: {}, parent_answers: nil, err: $stderr)
138
147
  sd = state_dir || Session.default_state_dir
139
148
  if attach || resume
140
149
  warn_memory_flags_ignored(attach || resume, memories, muted_memories, err, appended_prompt_files: appended_prompt_files,
@@ -156,7 +165,8 @@ module Samagotchi
156
165
  setup: SessionSetup.new(llm_context: llm_context.empty? ? nil : LLMContextOverride.update(nil, llm_context),
157
166
  thinking: Thinking.session_level(thinking),
158
167
  appended_prompt_files: appended_prompt_files,
159
- toolkit: toolkit, bundle_settings: bundle_settings))
168
+ toolkit: toolkit, bundle_settings: bundle_settings,
169
+ parent_answers: parent_answers))
160
170
  end
161
171
  client = BridgeClient.wait_for(session.id, session_dir: Session.session_dir(session.id, state_dir: sd), timeout: wait)
162
172
  client || raise(Error, "the worker for session #{session.id} did not start its Bridge in time")
@@ -592,10 +592,7 @@ module Samagotchi
592
592
  # The worker runs it and every UI renders its :command_ran, this one
593
593
  # too, so nothing waits here.
594
594
  def send_command(line)
595
- # A parent agent's /continue yes goes as chi answer's: the worker
596
- # holds it to guardrails.parent_answers.
597
- client_id = @as_parent && TurnFlow.continue_yes?(line) ? ClientId::CLI_ANSWER : @client_id
598
- reply = @client.post_command(line: line, client_id: client_id)
595
+ reply = @client.post_command(line: line, client_id: client_id_for(line))
599
596
  if reply.status == 202
600
597
  # Input from a pipe ends once its output is in (#command_ran).
601
598
  command_id = reply.json&.fetch("command_id", nil)
@@ -808,13 +805,27 @@ module Samagotchi
808
805
  steer_cut_note(ack["cut"].to_s.to_sym, ack["cut_after"])
809
806
  end
810
807
 
808
+ # The client id +line+ goes with, as a command (#send_command) or as a
809
+ # message the Bridge may run as one (#send_prompt: /cut and /queue
810
+ # text). A parent agent's /continue yes and /parent-answers go as chi
811
+ # answer's: the worker holds the first to guardrails.parent_answers and
812
+ # never takes the second as a user's loosening. The command is the
813
+ # registry's own match (the worker's), not a pattern of ours.
814
+ def client_id_for(line)
815
+ return @client_id unless @as_parent
816
+ return ClientId::CLI_ANSWER if TurnFlow.continue_yes?(line)
817
+ return ClientId::CLI_ANSWER if command_registry.lookup(line)&.id == :parent_answers
818
+
819
+ @client_id
820
+ end
821
+
811
822
  def send_prompt(text, delivery: nil)
812
823
  persist_recent_history(text)
813
824
  images = attach_images(text)
814
825
  return own_prompt_failed if images.nil?
815
826
 
816
827
  running = @running
817
- options = { prompt: text, client_id: @client_id }
828
+ options = { prompt: text, client_id: client_id_for(text) }
818
829
  options[:no_interrupt] = true if @no_interrupt
819
830
  options[:images] = images unless images.empty?
820
831
  options[:delivery] = delivery if delivery
@@ -105,10 +105,14 @@ module Samagotchi
105
105
  # @param bundle_settings [Hash] --set's values (BundleSessionSettings):
106
106
  # a new session's own; a resumed one keeps its own (--set is in
107
107
  # +toolkit_flags+ then)
108
+ # @param parent_answers [String, nil] --parent-answers: a new session's
109
+ # own guardrails.parent_answers; a resumed one keeps its own (named in
110
+ # +toolkit_flags+ then)
108
111
  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,
109
112
  spinner_tick_interval: AttachedView::TICK_INTERVAL, spinner_clock: nil, scratch: false, llm_context: {}, thinking: nil,
110
- appended_prompt_files: [], toolkit: nil, toolkit_flags: [], bundle_settings: {})
113
+ appended_prompt_files: [], toolkit: nil, toolkit_flags: [], bundle_settings: {}, parent_answers: nil)
111
114
  @scratch = scratch
115
+ @parent_answers = parent_answers
112
116
  @toolkit = toolkit
113
117
  @toolkit_flags = toolkit_flags
114
118
  @bundle_settings = bundle_settings
@@ -281,7 +285,7 @@ module Samagotchi
281
285
  muted_memory_names: @muted_memory_names,
282
286
  scratch: @scratch,
283
287
  setup: SessionSetup.new(appended_prompt_files: @appended_prompt_files, toolkit: @toolkit || Toolkit.configured,
284
- bundle_settings: @bundle_settings)
288
+ bundle_settings: @bundle_settings, parent_answers: @parent_answers)
285
289
  ))
286
290
  # --llm-context*: the session's own values, over a resumed one's.
287
291
  # Saved at once on a resumed one: leaving before a turn keeps them.
@@ -876,9 +880,11 @@ module Samagotchi
876
880
  command = if command_registry.anytime?(input)
877
881
  @engine.running_anytime { @commands.run(input, confirm: confirm) }
878
882
  else
879
- # A parent's /continue yes is held to guardrails.parent_answers.
880
- parent = TurnFlow.continue_yes?(input) && as_parent? ? { as_parent: true } : {}
881
- @commands.run(input, confirm: confirm, **parent)
883
+ # A parent's /continue yes is held to guardrails.parent_answers;
884
+ # only a person here (not piped, no agent marker) loosens
885
+ # /parent-answers.
886
+ parent = as_parent?
887
+ @commands.run(input, confirm: confirm, as_parent: parent && TurnFlow.continue_yes?(input), user_ui: !parent)
882
888
  end
883
889
  if command
884
890
  show_command_result(command)
@@ -134,7 +134,10 @@ module Samagotchi
134
134
  "(the most is #{max}, #{MAX_CHILDREN_KEY}): #{ids}. #{full_hint(peers)}"
135
135
  end
136
136
 
137
- setup = toolkit.empty? ? SessionSetup.new : SessionSetup.new(toolkit: Toolkit.new(name: toolkit))
137
+ # The parent's own parent_answers, as it is now (a snapshot): the
138
+ # child's answers are held at least as tight.
139
+ setup = SessionSetup.new(toolkit: toolkit.empty? ? nil : Toolkit.new(name: toolkit),
140
+ parent_answers: parent.parent_answers)
138
141
  child = SessionManager.spawn_session(prompt: task, working_directory: folder,
139
142
  model_name: child_model(model, parent), memories: CHILD_MEMORIES,
140
143
  parent_id: parent.id, delegate: true, setup: setup, state_dir: state_dir)
@@ -213,6 +213,10 @@ module Samagotchi
213
213
  # @return [String]
214
214
  def finish(wait, child_id, timeout:, report: false, reports: "off")
215
215
  status = ParentReport.status(wait)
216
+ # What the parent may pick on the child's question: config.yml
217
+ # tightened by the child session's own parent_answers (the session
218
+ # the wait read; none: config.yml's).
219
+ strategy = Guardrails::ParentPolicy.for_session(wait.session)
216
220
  case wait.status
217
221
  when :done
218
222
  reply_result(child_id, status, wait.text)
@@ -221,11 +225,11 @@ module Samagotchi
221
225
  when :stopped
222
226
  result(child_id, status, "the child was stopped (chi sessions stop); delegate with session: #{child_id} starts it again with a message")
223
227
  when :waiting_for_answer
224
- result(child_id, status, waiting_text(child_id, wait.question, report: report, reports: reports))
228
+ result(child_id, status, waiting_text(child_id, wait.question, report: report, reports: reports, strategy: strategy))
225
229
  when :canceled
226
230
  canceled_result(child_id, reports: reports)
227
231
  when :no_reply
228
- result(child_id, status, "#{no_reply_text(wait, child_id)}; its session shows what happened")
232
+ result(child_id, status, "#{no_reply_text(wait, child_id, strategy: strategy)}; its session shows what happened")
229
233
  when :worker_gone
230
234
  result(child_id, status,
231
235
  "the child's worker is gone (it stopped or crashed); delegate with session: #{child_id} starts it again with a message")
@@ -266,8 +270,8 @@ module Samagotchi
266
270
  end
267
271
 
268
272
  # ParentReport's line for a turn that left no reply, about the child.
269
- def no_reply_text(wait, child_id)
270
- ParentReport.no_reply_line(wait, child_id).sub(/\Athe turn/, "the child's turn")
273
+ def no_reply_text(wait, child_id, strategy: nil)
274
+ ParentReport.no_reply_line(wait, child_id, strategy: strategy).sub(/\Athe turn/, "the child's turn")
271
275
  end
272
276
 
273
277
  def result(child_id, status, text)
@@ -303,9 +307,9 @@ module Samagotchi
303
307
  # A report's last line differs (+report+): "waits again" made models
304
308
  # call delegate_result right after answering Continue.
305
309
  # The tool's last line follows #reports_mode.
306
- def waiting_text(child_id, pending, report: false, reports: "off")
307
- text = "Child #{child_id} is #{ParentReport.question_text(pending, session_id: child_id)}"
308
- if ParentReport.continue?(pending) && ParentReport.may_continue?(pending)
310
+ def waiting_text(child_id, pending, report: false, reports: "off", strategy: nil)
311
+ text = "Child #{child_id} is #{ParentReport.question_text(pending, session_id: child_id, strategy: strategy)}"
312
+ if ParentReport.continue?(pending) && ParentReport.may_continue?(pending, strategy: strategy)
309
313
  text += "The child ran out of steps before it answered. Decide: continue it (run the chi answer command " \
310
314
  "above with execute) if it is getting somewhere; send it a narrower follow-up with delegate " \
311
315
  "session: #{child_id} (that drops the question); or stop it and report back to your user.\n"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.86.0"
4
+ VERSION = "0.87.0"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end
@@ -794,7 +794,9 @@ module Samagotchi
794
794
  # as its output
795
795
  def run_command_line(command)
796
796
  @engine.holding_announcements do
797
- command_result(command) { @commands.run(command.line, confirm: confirm?(command), as_parent: as_parent?(command)) }
797
+ command_result(command) do
798
+ @commands.run(command.line, confirm: confirm?(command), as_parent: as_parent?(command), user_ui: user_ui?(command))
799
+ end
798
800
  end
799
801
  end
800
802
 
@@ -808,6 +810,12 @@ module Samagotchi
808
810
  # guardrails.parent_answers (SessionCommands#continue_answer).
809
811
  def as_parent?(command) = command.client_id.to_s == ClientId::CLI_ANSWER
810
812
 
813
+ # Whether +command+ is a user's at a UI (a web tab, a chi TUI, a card's
814
+ # click), not a parent agent's: only such a line loosens
815
+ # /parent-answers. chi send, a delegate's follow-up, a plugin and an
816
+ # input with no client never do (ParentAnswersCommand).
817
+ def user_ui?(command) = ClientId.interactive?(command.client_id) && !as_parent?(command)
818
+
811
819
  # The block's Result for +command+; one that raised, or found no
812
820
  # command for the line, is an error Result saying so.
813
821
  # @return [SessionCommands::Result]
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.86.0
4
+ version: 0.87.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -422,6 +422,7 @@ files:
422
422
  - lib/samagotchi/note_delivery.rb
423
423
  - lib/samagotchi/output_formatter.rb
424
424
  - lib/samagotchi/owner_lock.rb
425
+ - lib/samagotchi/parent_answers_command.rb
425
426
  - lib/samagotchi/parent_report.rb
426
427
  - lib/samagotchi/paths.rb
427
428
  - lib/samagotchi/peer_sources.rb