samagotchi 0.102.0 → 0.104.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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +93 -1
  3. data/bin/chi +60 -191
  4. data/docs/architecture.md +67 -1
  5. data/docs/cli.md +190 -9
  6. data/docs/configuration.md +7 -1
  7. data/docs/guardrails.md +4 -2
  8. data/docs/internals/context-telemetry.md +6 -2
  9. data/docs/internals/engine.md +5 -2
  10. data/docs/internals/llm-context-forget.md +11 -1
  11. data/docs/internals/participants.md +137 -0
  12. data/docs/internals/remote-sessions.md +429 -0
  13. data/docs/internals/working-set.md +95 -11
  14. data/docs/plugins.md +33 -3
  15. data/docs/sessions.md +16 -7
  16. data/docs/sub-agent.md +16 -0
  17. data/docs/testing.md +68 -4
  18. data/lib/samagotchi/answer_command.rb +27 -25
  19. data/lib/samagotchi/bridge.rb +31 -7
  20. data/lib/samagotchi/bridge_client/event_stream.rb +86 -12
  21. data/lib/samagotchi/bridge_client/stream_request.rb +77 -0
  22. data/lib/samagotchi/bridge_client.rb +26 -39
  23. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
  24. data/lib/samagotchi/bundles/claude-peer/scripts/pair +30 -1
  25. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
  26. data/lib/samagotchi/bundles/codex-peer/scripts/pair +49 -4
  27. data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +7 -4
  28. data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
  29. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  30. data/lib/samagotchi/chi_invocation.rb +40 -0
  31. data/lib/samagotchi/cli/command.rb +3 -2
  32. data/lib/samagotchi/cli/flags.rb +5 -0
  33. data/lib/samagotchi/cli/parent_wait.rb +22 -13
  34. data/lib/samagotchi/context_command.rb +80 -73
  35. data/lib/samagotchi/context_status.rb +15 -2
  36. data/lib/samagotchi/engine.rb +28 -14
  37. data/lib/samagotchi/guardrail_wiring.rb +7 -1
  38. data/lib/samagotchi/idle_recap.rb +8 -2
  39. data/lib/samagotchi/idle_scheduler.rb +34 -9
  40. data/lib/samagotchi/kernel_loop.rb +21 -2
  41. data/lib/samagotchi/launch_mode.rb +26 -0
  42. data/lib/samagotchi/llm/chat_loop.rb +8 -1
  43. data/lib/samagotchi/llm_context_forget.rb +13 -6
  44. data/lib/samagotchi/note_command.rb +8 -7
  45. data/lib/samagotchi/owner_lock.rb +65 -5
  46. data/lib/samagotchi/parent_report.rb +55 -43
  47. data/lib/samagotchi/participants.rb +122 -0
  48. data/lib/samagotchi/remote_attach_command.rb +119 -0
  49. data/lib/samagotchi/remote_command.rb +170 -0
  50. data/lib/samagotchi/remotes.rb +130 -0
  51. data/lib/samagotchi/run_options.rb +245 -0
  52. data/lib/samagotchi/send_command/busy.rb +58 -0
  53. data/lib/samagotchi/send_command/parent_gate.rb +23 -10
  54. data/lib/samagotchi/send_command/wait_target.rb +16 -3
  55. data/lib/samagotchi/send_command.rb +173 -108
  56. data/lib/samagotchi/session.rb +38 -9
  57. data/lib/samagotchi/session_access/contract.rb +167 -0
  58. data/lib/samagotchi/session_access/dispatch.rb +152 -0
  59. data/lib/samagotchi/session_access/http_transport.rb +109 -0
  60. data/lib/samagotchi/session_access/local.rb +400 -0
  61. data/lib/samagotchi/session_access/remote.rb +734 -0
  62. data/lib/samagotchi/session_access/web_bridge_client.rb +236 -0
  63. data/lib/samagotchi/session_access.rb +223 -0
  64. data/lib/samagotchi/session_archive_command.rb +9 -5
  65. data/lib/samagotchi/session_commands.rb +20 -15
  66. data/lib/samagotchi/session_continue.rb +1 -1
  67. data/lib/samagotchi/session_delete_command.rb +8 -4
  68. data/lib/samagotchi/session_inbox.rb +18 -9
  69. data/lib/samagotchi/session_manager.rb +27 -6
  70. data/lib/samagotchi/session_setup.rb +11 -6
  71. data/lib/samagotchi/sessions_command.rb +36 -14
  72. data/lib/samagotchi/share_command.rb +174 -0
  73. data/lib/samagotchi/terminal_ui/attach_launcher.rb +66 -17
  74. data/lib/samagotchi/terminal_ui/attach_link.rb +114 -0
  75. data/lib/samagotchi/terminal_ui/attached_loop.rb +155 -87
  76. data/lib/samagotchi/terminal_ui/formatting.rb +13 -1
  77. data/lib/samagotchi/terminal_ui/local_link.rb +135 -0
  78. data/lib/samagotchi/terminal_ui/remote_connector.rb +66 -0
  79. data/lib/samagotchi/terminal_ui/remote_link.rb +245 -0
  80. data/lib/samagotchi/terminal_ui/version_lines.rb +28 -3
  81. data/lib/samagotchi/terminal_ui.rb +6 -3
  82. data/lib/samagotchi/testing/waiting.rb +92 -9
  83. data/lib/samagotchi/title_command.rb +9 -11
  84. data/lib/samagotchi/tools/task_runtime.rb +17 -3
  85. data/lib/samagotchi/version.rb +1 -1
  86. data/lib/samagotchi/web/app.rb +530 -126
  87. data/lib/samagotchi/web/grants.rb +302 -0
  88. data/lib/samagotchi/web/participant_access.rb +286 -0
  89. data/lib/samagotchi/web/principal.rb +29 -0
  90. data/lib/samagotchi/web/public/data.js +3 -2
  91. data/lib/samagotchi/web/public/index.html +20 -4
  92. data/lib/samagotchi/web/public/stage_frame.js +7 -2
  93. data/lib/samagotchi/web/public/stage_view.js +3 -2
  94. data/lib/samagotchi/web/public/stream_handlers.js +3 -1
  95. data/lib/samagotchi/web/public/working_set.js +159 -28
  96. data/lib/samagotchi/web/public/working_set_rail.js +39 -12
  97. data/lib/samagotchi/web/public/working_set_view.js +91 -33
  98. data/lib/samagotchi/web/server.rb +3 -1
  99. data/lib/samagotchi/web/session_access_routes.rb +459 -0
  100. data/lib/samagotchi/worker.rb +26 -5
  101. data/lib/samagotchi/worker_sidecar.rb +12 -5
  102. data/lib/samagotchi/working_set.rb +88 -14
  103. data/lib/samagotchi/working_set_command.rb +133 -30
  104. data/lib/samagotchi/working_set_pin.rb +34 -0
  105. data/lib/samagotchi/working_set_steer.rb +191 -27
  106. metadata +28 -1
@@ -8,6 +8,7 @@ require_relative "../thinking_command"
8
8
  require_relative "../bridge_client"
9
9
  require_relative "../parent_report"
10
10
  require_relative "../cli/exit"
11
+ require_relative "../launch_mode"
11
12
  require_relative "live_region"
12
13
  require_relative "plain_surface"
13
14
  require_relative "attached_loop"
@@ -23,6 +24,9 @@ module Samagotchi
23
24
  class Error < StandardError; end
24
25
 
25
26
  BRIDGE_WAIT = 10.0
27
+ # A connector that answered no session (its own failures raise Error
28
+ # with their words).
29
+ NOTHING_CONNECTED = "could not attach: no session was found or started"
26
30
 
27
31
  module_function
28
32
 
@@ -59,17 +63,27 @@ module Samagotchi
59
63
  # loosening route from a command line)
60
64
  # @param state_dir [String, nil] where the sessions are (the default
61
65
  # state dir by default): the worker's, for the attached loop too
66
+ # @param connector [#connect, nil] finds or starts the session
67
+ # elsewhere: its #connect returns the AttachLink the loop runs on
68
+ # (nil: this machine's, through .connect; a connector never falls
69
+ # back to it), and its #client_id, when it has one, names this
70
+ # TUI's streams and requests there (#client_id)
62
71
  # @return [Symbol] :detached, :closed when the worker went away,
63
72
  # :failed when the --model switch didn't go through, or (input from a
64
73
  # pipe) :turn_failed / :empty_answer / :unanswered (AttachedLoop#run)
65
74
  def run(attach: nil, shared: false, resume: nil, prompt: nil, model: nil, no_interrupt: false, default_input: true,
66
75
  memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], toolkit: nil,
67
- toolkit_flags: [], settings: nil, parent_answers: nil, state_dir: nil)
76
+ toolkit_flags: [], settings: nil, parent_answers: nil, state_dir: nil, connector: nil)
68
77
  state_dir ||= Session.default_state_dir
69
- client = connect(attach: attach, shared: shared, resume: resume, model: model, state_dir: state_dir,
70
- memories: memories, muted_memories: muted_memories, llm_context: llm_context, thinking: thinking,
71
- appended_prompt_files: appended_prompt_files, toolkit: toolkit, toolkit_flags: toolkit_flags,
72
- settings: settings, parent_answers: parent_answers)
78
+ link = connector && (connector.connect || raise(Error, NOTHING_CONNECTED))
79
+ client = if connector
80
+ link.client
81
+ else
82
+ connect(attach: attach, shared: shared, resume: resume, model: model, state_dir: state_dir,
83
+ memories: memories, muted_memories: muted_memories, llm_context: llm_context,
84
+ thinking: thinking, appended_prompt_files: appended_prompt_files, toolkit: toolkit,
85
+ toolkit_flags: toolkit_flags, settings: settings, parent_answers: parent_answers)
86
+ end
73
87
  commands = []
74
88
  commands << "/model #{model}" if model && (attach || resume)
75
89
  commands << LLMContextOverride.command_line(llm_context) if (attach || resume) && !llm_context.empty?
@@ -77,21 +91,39 @@ module Samagotchi
77
91
  commands << "#{ThinkingCommand::NAME} #{thinking} --yes" if (attach || resume) && thinking
78
92
  first_command = commands.size > 1 ? commands : commands.first
79
93
  surface = open_surface
80
- attached = AttachedLoop.new(client: client, screen: surface, client_id: "tui:#{Process.pid}", first_prompt: prompt,
81
- first_command: first_command, no_interrupt: no_interrupt,
82
- default_input: default_input && !prompt && !attach && !resume,
83
- wait_at_eof: !$stdin.tty?,
84
- as_parent: Guardrails::ParentApprovals.parent_process?, state_dir: state_dir)
94
+ options = { screen: surface, client_id: client_id(connector), first_prompt: prompt,
95
+ first_command: first_command, no_interrupt: no_interrupt,
96
+ default_input: default_input && !prompt && !attach && !resume,
97
+ wait_at_eof: !$stdin.tty?,
98
+ as_parent: Guardrails::ParentApprovals.parent_process? }
99
+ attached = if connector
100
+ AttachedLoop.new(link: link, **options)
101
+ else
102
+ AttachedLoop.new(client: client, **options, state_dir: state_dir)
103
+ end
85
104
  ended = begin
86
105
  attached.run
87
106
  ensure
88
107
  close_surface(surface)
89
108
  end
90
109
  if ended == :unanswered && attached.unanswered
91
- report_unanswered(attached.unanswered, session_id: client.session_id, state_dir: state_dir)
110
+ # A link's strategy is read where the session is, now (L4), its
111
+ # commands spelled its way.
112
+ where = connector ? { strategy: link.parent_strategy, via: link.via } : {}
113
+ report_unanswered(attached.unanswered, session_id: client.session_id, state_dir: state_dir, **where)
92
114
  end
93
115
  report_ended(ended)
94
116
  ended
117
+ ensure
118
+ link&.close
119
+ end
120
+
121
+ # This TUI's client id: tui:PID, or the connector's own (a remote one
122
+ # adds the host, for the UIs there).
123
+ def client_id(connector)
124
+ return connector.client_id if connector.respond_to?(:client_id)
125
+
126
+ "tui:#{Process.pid}"
95
127
  end
96
128
 
97
129
  # A question left waiting (exit 3), in full on stderr as `chi send
@@ -100,10 +132,16 @@ module Samagotchi
100
132
  # @param pending [Hash] the pending question as the Bridge sent it
101
133
  # @param state_dir [String, nil] where the session file is: what a
102
134
  # parent may pick follows its own parent_answers too
103
- def report_unanswered(pending, session_id:, err: $stderr, state_dir: nil)
135
+ # @param strategy [Guardrails::ParentPolicy::Strategy, nil] that
136
+ # strategy, read already (an AttachLink's); nil:
137
+ # ParentReport.strategy_for here
138
+ # @param via [ChiInvocation, nil] how its commands are spelled (nil:
139
+ # this machine's)
140
+ def report_unanswered(pending, session_id:, err: $stderr, state_dir: nil, strategy: nil, via: nil)
104
141
  question = PendingQuestion.from_file(pending)
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)}")
142
+ strategy ||= ParentReport.strategy_for(session_id, state_dir: state_dir || Session.default_state_dir)
143
+ words = via ? { via: via } : {}
144
+ err.print("chi: #{ParentReport.question_text(question, session_id: session_id, strategy: strategy, **words)}")
107
145
  err.flush
108
146
  end
109
147
 
@@ -157,6 +195,7 @@ module Samagotchi
157
195
  resume = attach
158
196
  end
159
197
 
198
+ await_leaving_worker(resume, sd, err) if resume
160
199
  session = if resume
161
200
  SessionManager.resume_session(resume, state_dir: sd)
162
201
  else
@@ -169,13 +208,23 @@ module Samagotchi
169
208
  parent_answers: parent_answers))
170
209
  end
171
210
  client = BridgeClient.wait_for(session.id, session_dir: Session.session_dir(session.id, state_dir: sd), timeout: wait)
172
- client || raise(Error, "the worker for session #{session.id} did not start its Bridge in time")
173
- rescue SessionManager::OwnedByTUI
174
- raise Error, "session #{resume} is open in a chi REPL; close it there first"
211
+ client || raise(Error, format(LaunchMode::BRIDGE_LATE, session.id))
212
+ rescue SessionManager::OwnedByTUI => e
213
+ raise Error, e.message
175
214
  rescue ArgumentError => e
176
215
  raise Error, e.message
177
216
  end
178
217
 
218
+ # The session's previous worker may still be leaving: its Bridge
219
+ # closed, the lock held while it writes its recap on leave (a resume
220
+ # now would start nothing, the lock taken). Wait for it to let go
221
+ # (OwnerLock.await_release), with one line saying so; any other owner
222
+ # is left to resume_session as before.
223
+ def await_leaving_worker(session_id, state_dir, err)
224
+ OwnerLock.await_release(Session.session_dir(session_id, state_dir: state_dir),
225
+ on_wait: ->(_owner) { err.puts(LaunchMode::RECAP_WAIT) })
226
+ end
227
+
179
228
  # --model as typed: spawn_session stores the resolved ref and this name.
180
229
  def model_ref(model)
181
230
  ModelProfile.required_model_name(model)
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Samagotchi
4
+ class TerminalUI
5
+ # What the attached TUI (AttachedLoop) knows about *where* its session
6
+ # is, beside the client it sends requests with: how the worker is found
7
+ # again, what the worker can do, the session's files it reads (images,
8
+ # saved records, children), the session calls it makes (delete,
9
+ # archive), and how the commands it prints are spelled. LocalLink is
10
+ # this machine's (the session's state dir, its worker's Bridge); a
11
+ # remote attach plugs in another.
12
+ #
13
+ # Each method here raises NotImplementedError until a link defines it.
14
+ module AttachLink
15
+ # Which Tab completions the TUI offers beside the / commands.
16
+ # @!attribute paths [Boolean] @path: this machine's files
17
+ # @!attribute memories [Boolean] #name: this machine's memories
18
+ Completions = Data.define(:paths, :memories)
19
+
20
+ class Completions
21
+ ALL = new(paths: true, memories: true)
22
+ end
23
+
24
+ # The port a worker_changed event names (string or symbol keys; nil
25
+ # for a worker reached another way).
26
+ def self.event_port(event) = event[:port] || event["port"]
27
+
28
+ # @return [#session_id, #follow, #post_turn, ...] the session's client
29
+ # (a BridgeClient on this machine), the newest after #switched
30
+ def client = raise(NotImplementedError)
31
+
32
+ # The session's live worker for the stream to follow (EventStream's
33
+ # rediscover).
34
+ # @return [Array(Object, String)] [a client, nil] for a live one,
35
+ # [nil, nil] while there is none yet, or [:gone, reason] once no
36
+ # worker is coming
37
+ def live_worker = raise(NotImplementedError)
38
+
39
+ # The stream moved to a new worker (its worker_changed event, string
40
+ # or symbol keys): requests go there too.
41
+ # @return [Object] the client from now on (#client)
42
+ def switched(_event) = raise(NotImplementedError)
43
+
44
+ # Whether the worker runs delivery: queue (/queue).
45
+ def queues? = raise(NotImplementedError)
46
+
47
+ # Whether the worker takes queued messages back (/unqueue).
48
+ def unqueues? = raise(NotImplementedError)
49
+
50
+ # The session's metrics for /stats, nil when the worker did not answer.
51
+ # @return [Hash, nil] string keys
52
+ # @raise [IOError] the session's machine couldn't be asked (a remote's
53
+ # Unreachable: the loop shows its message)
54
+ def stats_metrics = raise(NotImplementedError)
55
+
56
+ # Store a prompt's @path images where the worker reads them.
57
+ # @param paths [Array<String>] the files as typed
58
+ # @return [Array<Hash>] refs ({file:, name:}) for the turn's images
59
+ # @raise [ImageStore::Error] one can't be used
60
+ def ingest_images(_paths) = raise(NotImplementedError)
61
+
62
+ # A turn's saved tool records ([] when none, never raises).
63
+ # @return [Array<Hash>]
64
+ def saved_tool_records(_turn_id) = raise(NotImplementedError)
65
+
66
+ # A turn's saved record (nil when none, never raises).
67
+ # @return [Hash, nil]
68
+ def saved_turn_record(_turn_id) = raise(NotImplementedError)
69
+
70
+ # The session's delegates' counts, the status row's children segment.
71
+ # @return [ChildrenStatus::Counts]
72
+ def children_counts = raise(NotImplementedError)
73
+
74
+ # /exit --delete, once the worker let go.
75
+ def delete_session = raise(NotImplementedError)
76
+
77
+ # /archive: SessionManager.archive_session's result ({discarded: […]}).
78
+ # @return [Hash]
79
+ def archive_session = raise(NotImplementedError)
80
+
81
+ # The newest chi installed where the worker runs, for the version line.
82
+ # @return [String, nil]
83
+ def installed_version = raise(NotImplementedError)
84
+
85
+ # The config's default model where the worker runs, as its /model
86
+ # names it.
87
+ # @return [String, nil]
88
+ def default_model_name = raise(NotImplementedError)
89
+
90
+ # What a parent agent may pick on the session's questions, read now
91
+ # (ParentReport.strategy_for).
92
+ def parent_strategy = raise(NotImplementedError)
93
+
94
+ # @return [Completions]
95
+ def completions = raise(NotImplementedError)
96
+
97
+ # The stream's sleeps between failed reconnects.
98
+ # @return [Array<Float>]
99
+ def reconnect_delays = raise(NotImplementedError)
100
+
101
+ # How the commands the TUI prints are spelled.
102
+ # @return [ChiInvocation]
103
+ def via = raise(NotImplementedError)
104
+
105
+ # Why the session's machine couldn't be asked the last time (for the
106
+ # stream's closing line), nil when there is nothing more to say.
107
+ # @return [String, nil]
108
+ def failure = raise(NotImplementedError)
109
+
110
+ # The attach is over (AttachLauncher.run): let go of what it held.
111
+ def close = raise(NotImplementedError)
112
+ end
113
+ end
114
+ end