samagotchi 0.63.0 → 0.65.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -1
  3. data/docs/architecture.md +32 -5
  4. data/docs/cli.md +41 -9
  5. data/docs/configuration.md +143 -11
  6. data/docs/internals/engine.md +25 -6
  7. data/docs/plugins.md +7 -0
  8. data/docs/sessions.md +14 -10
  9. data/docs/sub-agent.md +21 -4
  10. data/docs/testing.md +6 -0
  11. data/lib/samagotchi/answer_command.rb +5 -1
  12. data/lib/samagotchi/bridge/turn_accumulator.rb +1 -0
  13. data/lib/samagotchi/bridge.rb +29 -5
  14. data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
  15. data/lib/samagotchi/bundles/slow-commands/plugin.rb +5 -3
  16. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  17. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  18. data/lib/samagotchi/child_reports.rb +7 -2
  19. data/lib/samagotchi/cli/parent_wait.rb +9 -2
  20. data/lib/samagotchi/client.rb +4 -2
  21. data/lib/samagotchi/composer_route.rb +90 -0
  22. data/lib/samagotchi/config.rb +11 -7
  23. data/lib/samagotchi/continue_offer.rb +17 -3
  24. data/lib/samagotchi/delivery.rb +13 -8
  25. data/lib/samagotchi/engine.rb +41 -7
  26. data/lib/samagotchi/generation_phase.rb +17 -0
  27. data/lib/samagotchi/host_config.rb +33 -12
  28. data/lib/samagotchi/host_model.rb +2 -2
  29. data/lib/samagotchi/host_registry.rb +34 -11
  30. data/lib/samagotchi/idle_client.rb +62 -25
  31. data/lib/samagotchi/idle_recap.rb +5 -2
  32. data/lib/samagotchi/idle_target.rb +30 -7
  33. data/lib/samagotchi/interrupted_turn.rb +7 -3
  34. data/lib/samagotchi/last_turn.rb +5 -2
  35. data/lib/samagotchi/llm/adapters.rb +41 -0
  36. data/lib/samagotchi/llm/anthropic_assembly.rb +210 -0
  37. data/lib/samagotchi/llm/anthropic_messages.rb +416 -0
  38. data/lib/samagotchi/llm/anthropic_wire.rb +238 -0
  39. data/lib/samagotchi/llm/api_key.rb +26 -9
  40. data/lib/samagotchi/llm/chat_loop.rb +113 -14
  41. data/lib/samagotchi/llm/dialect.rb +57 -0
  42. data/lib/samagotchi/llm/errors.rb +81 -12
  43. data/lib/samagotchi/llm/model_listing.rb +111 -0
  44. data/lib/samagotchi/llm/model_result.rb +9 -2
  45. data/lib/samagotchi/llm/openai_chat.rb +46 -93
  46. data/lib/samagotchi/model_profile.rb +1 -1
  47. data/lib/samagotchi/pending_input_queue.rb +10 -3
  48. data/lib/samagotchi/plugin/sessions.rb +27 -5
  49. data/lib/samagotchi/reasoning_history.rb +14 -4
  50. data/lib/samagotchi/recap_setup.rb +2 -1
  51. data/lib/samagotchi/reply_wait.rb +171 -6
  52. data/lib/samagotchi/self_report.rb +18 -28
  53. data/lib/samagotchi/send_command.rb +63 -19
  54. data/lib/samagotchi/session.rb +3 -2
  55. data/lib/samagotchi/session_commands.rb +1 -1
  56. data/lib/samagotchi/session_inbox.rb +63 -7
  57. data/lib/samagotchi/session_manager.rb +49 -20
  58. data/lib/samagotchi/session_metrics.rb +6 -11
  59. data/lib/samagotchi/steer_cut.rb +4 -0
  60. data/lib/samagotchi/terminal_ui/attached_loop.rb +55 -28
  61. data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -1
  62. data/lib/samagotchi/terminal_ui/formatting.rb +7 -22
  63. data/lib/samagotchi/terminal_ui/status_row.rb +24 -3
  64. data/lib/samagotchi/terminal_ui.rb +72 -57
  65. data/lib/samagotchi/tool_activity.rb +5 -5
  66. data/lib/samagotchi/tools/delegate.rb +28 -1
  67. data/lib/samagotchi/tools/delegate_cursor.rb +9 -4
  68. data/lib/samagotchi/tools/delegate_wait.rb +17 -5
  69. data/lib/samagotchi/tools/execute.rb +11 -4
  70. data/lib/samagotchi/turn_checkpoint.rb +9 -3
  71. data/lib/samagotchi/turn_note.rb +22 -5
  72. data/lib/samagotchi/version.rb +1 -1
  73. data/lib/samagotchi/web/app.rb +2 -1
  74. data/lib/samagotchi/web/public/activity.js +3 -0
  75. data/lib/samagotchi/web/public/app.js +96 -13
  76. data/lib/samagotchi/web/public/commands.js +8 -8
  77. data/lib/samagotchi/web/public/composer.js +3 -3
  78. data/lib/samagotchi/web/public/delivery_line.js +228 -0
  79. data/lib/samagotchi/web/public/index.html +29 -0
  80. data/lib/samagotchi/web/public/live_turn.js +8 -0
  81. data/lib/samagotchi/web/public/stage_model.js +27 -7
  82. data/lib/samagotchi/web/public/stage_view.js +32 -10
  83. data/lib/samagotchi/web/public/stream_handlers.js +8 -2
  84. data/lib/samagotchi/web/public/timing.js +2 -1
  85. data/lib/samagotchi/web/public/turn_events.js +22 -8
  86. data/lib/samagotchi/web/public/turn_html.js +20 -4
  87. data/lib/samagotchi/web/public/turn_model.js +29 -12
  88. data/lib/samagotchi/web/public/turn_view.js +6 -4
  89. data/lib/samagotchi/worker.rb +7 -5
  90. metadata +9 -1
@@ -7,7 +7,8 @@
7
7
  // Rules:
8
8
  // - turn_started opens the first gen (no iteration yet).
9
9
  // - generation_started(n) opens a new gen unless the current one is still
10
- // empty (then it takes n).
10
+ // empty (then it takes n); either way the gen's startedAt is the page's
11
+ // clock at it (null for a replay).
11
12
  // - a chunk goes to the gen of its iteration (opening one when none has it);
12
13
  // a chunk with no iteration goes to the current gen, unless that one is
13
14
  // textDone or already has tool rows (an older server's replay), which
@@ -45,7 +46,7 @@ function isEmpty(gen) {
45
46
 
46
47
  function openGen(turn, iteration) {
47
48
  const closed = closeCurrent(turn);
48
- const gen = { iteration: iteration ?? null, thinking: "", text: "", activity: newActivity(), textDone: false, closed: false, notices: [] };
49
+ const gen = { iteration: iteration ?? null, thinking: "", text: "", activity: newActivity(), textDone: false, closed: false, notices: [], startedAt: null, thinkingAt: null };
49
50
  gen.tools = gen.activity.rows;
50
51
  turn.gens.push(gen);
51
52
  return { gen, opened: true, closed };
@@ -81,16 +82,21 @@ export function turnStarted(turn) {
81
82
  return openGen(turn, null);
82
83
  }
83
84
 
84
- export function generationStarted(turn, iteration = null) {
85
+ // +now+ (ms): when the page saw the generation start, its startedAt (the
86
+ // composer's delivery line clocks the step from it); null for a replayed
87
+ // start (a join: the snapshot's generation age stands in).
88
+ export function generationStarted(turn, iteration = null, now = null) {
85
89
  const current = currentGen(turn);
86
- if (current && isEmpty(current)) {
87
- current.iteration = iteration ?? current.iteration;
88
- return { gen: current, opened: false, closed: null };
89
- }
90
- return openGen(turn, iteration);
90
+ const r = current && isEmpty(current) ? { gen: current, opened: false, closed: null } : openGen(turn, iteration);
91
+ if (!r.opened) r.gen.iteration = iteration ?? r.gen.iteration;
92
+ r.gen.startedAt = now;
93
+ return r;
91
94
  }
92
95
 
93
- export function chunk(turn, { text = "", thinking = "", iteration = null } = {}) {
96
+ // +now+ (ms): when the page got the chunk (null for a replay): a gen's
97
+ // first thinking delta stamps its thinkingAt, the clock the worker's cut
98
+ // counts from too (GenerationPhase).
99
+ export function chunk(turn, { text = "", thinking = "", iteration = null, now = null } = {}) {
94
100
  let r;
95
101
  if (iteration == null) {
96
102
  const current = currentGen(turn);
@@ -102,6 +108,7 @@ export function chunk(turn, { text = "", thinking = "", iteration = null } = {})
102
108
  // Leading whitespace (the "\n" after <think>, or after a closed
103
109
  // think/tool_call block) would render as an empty first line; trim it at
104
110
  // the start of a gen's thinking and text only.
111
+ if (thinking && !gen.thinking && now != null) gen.thinkingAt = now;
105
112
  if (thinking) gen.thinking += leadTrimmed(gen.thinking, thinking);
106
113
  if (text) gen.text += leadTrimmed(gen.text, text);
107
114
  return r;
@@ -222,13 +229,23 @@ export function genLabelParts(gen) {
222
229
  if (line) return { head: cut(line), calls, repeats: true };
223
230
  if (first?.title) return { head: cut(`${toolName(first)} ${first.title}`), calls, repeats: true };
224
231
  if (gen.tools.length) {
225
- const names = [...new Set(gen.tools.map(toolName))];
226
- const head = `working with ${names.slice(0, LABEL_TOOLS).join(", ")}${names.length > LABEL_TOOLS ? ", …" : ""}`;
227
- return { head, calls, repeats: false };
232
+ // A plugin label that is a verb ("echoing", the default "calling
233
+ // tool") is the head itself when every call has one; among other
234
+ // tools it gives way to its tool's name ("working with read, echo").
235
+ const verbs = gen.tools.every(isVerbLabel);
236
+ const names = [...new Set(gen.tools.map((r) => (verbs || !isVerbLabel(r) ? toolName(r) : r.tool || "tool")))];
237
+ const list = `${names.slice(0, LABEL_TOOLS).join(", ")}${names.length > LABEL_TOOLS ? ", …" : ""}`;
238
+ return { head: verbs ? list : `working with ${list}`, calls, repeats: false };
228
239
  }
229
240
  return { head: "thinking", calls: "", repeats: false };
230
241
  }
231
242
 
243
+ // A plugin tool's label that reads as a verb (its first word ends in
244
+ // "ing": "echoing", "calling tool"), not as a name ("chrome: screenshot").
245
+ function isVerbLabel(row) {
246
+ return /^[a-z]+ing\b/i.test(String(row?.label || ""));
247
+ }
248
+
232
249
  // A step that said something (its text is not the turn's answer): a row of
233
250
  // its own in a finished block's outline.
234
251
  export function isNarrated(gen) {
@@ -700,14 +700,16 @@ export function createTurnView({
700
700
  draw(entry);
701
701
  }
702
702
  },
703
- generationStarted(data) {
703
+ // +now+: the page's clock at a live start (stream_handlers.js), null
704
+ // for a replayed one.
705
+ generationStarted(data, now = null) {
704
706
  if (!turn) this.turnStarted({});
705
- apply(model.generationStarted(turn, data.iteration ?? null));
707
+ apply(model.generationStarted(turn, data.iteration ?? null, now));
706
708
  },
707
- chunk({ text, thinking, iteration }) {
709
+ chunk({ text, thinking, iteration, now = null }) {
708
710
  if (!text && !thinking) return;
709
711
  if (!turn) this.turnStarted({});
710
- const r = model.chunk(turn, { text, thinking, iteration });
712
+ const r = model.chunk(turn, { text, thinking, iteration, now });
711
713
  apply(r);
712
714
  scheduleFlush(r.gen);
713
715
  },
@@ -900,7 +900,7 @@ module Samagotchi
900
900
  @reporting_turn = reports_to_parent?(turn_args[:origin])
901
901
  restores = @turn_flow.restores
902
902
  begin
903
- @checkpoint.begin_turn(prompt: prompt)
903
+ @checkpoint.begin_turn(prompt: prompt, enqueued_id: turn_args[:origin]&.dig(:enqueued_id))
904
904
  @session.status = Session::STATUS_RUNNING
905
905
  # A continue that moved this session to the chain's new link wrote
906
906
  # the new parent beside it (Session::PARENT_FILE): this turn's
@@ -929,7 +929,7 @@ module Samagotchi
929
929
  # no reply: its text is what came before its last tool calls, and a
930
930
  # wait (chi send --wait) gets the question instead.
931
931
  unless response.nil? || response.strip.empty? || @continue_offer.awaiting?
932
- SessionInbox.write_output(@session_dir, response)
932
+ SessionInbox.write_output(@session_dir, response, enqueued_id: turn_args[:origin]&.dig(:enqueued_id))
933
933
  end
934
934
  save_or_log(:turn) { save_session }
935
935
  # The reports the turn merged were read unless its end took the turn
@@ -1045,10 +1045,12 @@ module Samagotchi
1045
1045
  @pending_input_drain ||= lambda do
1046
1046
  # An iteration boundary: tell whoever sent a command now that it waits.
1047
1047
  refuse_queued_commands(mid_turn: true)
1048
- # A line with images runs as its own next turn (steering merges text
1049
- # only), and so does anything queued after it, to keep the order.
1048
+ # A queued line or one with images runs as its own turn after this
1049
+ # one (steering merges text only): skipped here, not a barrier, so a
1050
+ # line sent after it still goes in at this boundary. Only the files
1051
+ # claimed below reach @merged_this_turn and the checkpoint.
1050
1052
  files = SessionInbox.find_new_input_files(@session_dir).sort
1051
- .take_while { |input_file| !SessionInbox.waits_for_turn_end?(input_file) }
1053
+ .reject { |input_file| SessionInbox.waits_for_turn_end?(input_file) }
1052
1054
  merged = files.filter_map do |input_file|
1053
1055
  claimed_file = SessionInbox.claim_input_file(input_file)
1054
1056
  next unless claimed_file
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.63.0
4
+ version: 0.65.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -235,6 +235,7 @@ files:
235
235
  - lib/samagotchi/client_id.rb
236
236
  - lib/samagotchi/command_steps.rb
237
237
  - lib/samagotchi/commands/registry.rb
238
+ - lib/samagotchi/composer_route.rb
238
239
  - lib/samagotchi/config.rb
239
240
  - lib/samagotchi/config_text_edit.rb
240
241
  - lib/samagotchi/context_absorber.rb
@@ -320,11 +321,17 @@ files:
320
321
  - lib/samagotchi/last_turn.rb
321
322
  - lib/samagotchi/launch_mode.rb
322
323
  - lib/samagotchi/live_versions.rb
324
+ - lib/samagotchi/llm/adapters.rb
325
+ - lib/samagotchi/llm/anthropic_assembly.rb
326
+ - lib/samagotchi/llm/anthropic_messages.rb
327
+ - lib/samagotchi/llm/anthropic_wire.rb
323
328
  - lib/samagotchi/llm/api_key.rb
324
329
  - lib/samagotchi/llm/backend.rb
325
330
  - lib/samagotchi/llm/chat_loop.rb
331
+ - lib/samagotchi/llm/dialect.rb
326
332
  - lib/samagotchi/llm/errors.rb
327
333
  - lib/samagotchi/llm/http.rb
334
+ - lib/samagotchi/llm/model_listing.rb
328
335
  - lib/samagotchi/llm/model_result.rb
329
336
  - lib/samagotchi/llm/native_backend.rb
330
337
  - lib/samagotchi/llm/native_tool_normalizer.rb
@@ -554,6 +561,7 @@ files:
554
561
  - lib/samagotchi/web/public/copy.js
555
562
  - lib/samagotchi/web/public/ctx.js
556
563
  - lib/samagotchi/web/public/data.js
564
+ - lib/samagotchi/web/public/delivery_line.js
557
565
  - lib/samagotchi/web/public/diff_view.js
558
566
  - lib/samagotchi/web/public/families_view.js
559
567
  - lib/samagotchi/web/public/favicon.svg