samagotchi 0.50.0 → 0.52.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -1
  3. data/README.md +1 -1
  4. data/bin/chi +11 -3
  5. data/docs/architecture.md +1 -1
  6. data/docs/cli.md +40 -11
  7. data/docs/configuration.md +63 -21
  8. data/docs/hooks.md +3 -2
  9. data/docs/internals/engine.md +4 -2
  10. data/docs/memory.md +11 -0
  11. data/docs/plugins.md +2 -1
  12. data/docs/sessions.md +10 -5
  13. data/docs/testing.md +6 -1
  14. data/lib/samagotchi/bootstrap/config_writer.rb +6 -37
  15. data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
  16. data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
  17. data/lib/samagotchi/bridge.rb +29 -17
  18. data/lib/samagotchi/bridge_client.rb +3 -1
  19. data/lib/samagotchi/broadcast/recipients.rb +1 -2
  20. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  21. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  22. data/lib/samagotchi/children_status.rb +1 -1
  23. data/lib/samagotchi/commands/registry.rb +2 -1
  24. data/lib/samagotchi/config.rb +50 -182
  25. data/lib/samagotchi/config_text_edit.rb +8 -32
  26. data/lib/samagotchi/context_sources.rb +0 -7
  27. data/lib/samagotchi/context_window.rb +2 -2
  28. data/lib/samagotchi/continue_offer.rb +5 -4
  29. data/lib/samagotchi/cut_policy.rb +0 -1
  30. data/lib/samagotchi/delivery.rb +56 -0
  31. data/lib/samagotchi/engine.rb +48 -17
  32. data/lib/samagotchi/engine_peers.rb +1 -0
  33. data/lib/samagotchi/host_config.rb +183 -0
  34. data/lib/samagotchi/host_registry.rb +21 -13
  35. data/lib/samagotchi/kernel_loop.rb +28 -2
  36. data/lib/samagotchi/last_turn.rb +48 -0
  37. data/lib/samagotchi/live_versions.rb +0 -6
  38. data/lib/samagotchi/llm/native_tool_normalizer.rb +2 -7
  39. data/lib/samagotchi/llm_context_override.rb +3 -22
  40. data/lib/samagotchi/llm_context_strategy.rb +50 -16
  41. data/lib/samagotchi/memory_bundle/placeholder.rb +0 -5
  42. data/lib/samagotchi/model_profile.rb +4 -4
  43. data/lib/samagotchi/model_settings.rb +46 -0
  44. data/lib/samagotchi/plugin/context.rb +3 -3
  45. data/lib/samagotchi/plugin/sessions.rb +2 -2
  46. data/lib/samagotchi/queued_command.rb +35 -0
  47. data/lib/samagotchi/reminder_store.rb +0 -27
  48. data/lib/samagotchi/reply_wait.rb +11 -13
  49. data/lib/samagotchi/sampling_settings.rb +4 -4
  50. data/lib/samagotchi/self_report.rb +8 -5
  51. data/lib/samagotchi/send_command.rb +92 -21
  52. data/lib/samagotchi/session.rb +45 -24
  53. data/lib/samagotchi/session_commands.rb +41 -3
  54. data/lib/samagotchi/session_inbox.rb +36 -7
  55. data/lib/samagotchi/session_manager.rb +21 -10
  56. data/lib/samagotchi/session_metrics.rb +25 -6
  57. data/lib/samagotchi/session_setup.rb +30 -0
  58. data/lib/samagotchi/sessions_command.rb +1 -1
  59. data/lib/samagotchi/steer_cut.rb +21 -10
  60. data/lib/samagotchi/terminal_ui/attach_launcher.rb +10 -4
  61. data/lib/samagotchi/terminal_ui/attached_loop.rb +48 -1
  62. data/lib/samagotchi/terminal_ui/formatting.rb +69 -4
  63. data/lib/samagotchi/terminal_ui.rb +56 -8
  64. data/lib/samagotchi/thinking.rb +67 -7
  65. data/lib/samagotchi/thinking_command.rb +72 -0
  66. data/lib/samagotchi/tool_activity.rb +3 -0
  67. data/lib/samagotchi/tool_runner.rb +2 -0
  68. data/lib/samagotchi/tools/builtin_calls.rb +33 -0
  69. data/lib/samagotchi/tools/builtins.rb +7 -2
  70. data/lib/samagotchi/tools/peers.rb +3 -2
  71. data/lib/samagotchi/tools/task_runtime.rb +1 -1
  72. data/lib/samagotchi/version.rb +1 -1
  73. data/lib/samagotchi/vision_support.rb +4 -3
  74. data/lib/samagotchi/web/app.rb +59 -7
  75. data/lib/samagotchi/web/message_parts.rb +3 -0
  76. data/lib/samagotchi/web/public/activity.js +6 -0
  77. data/lib/samagotchi/web/public/app.js +199 -48
  78. data/lib/samagotchi/web/public/data.js +8 -4
  79. data/lib/samagotchi/web/public/format.js +8 -4
  80. data/lib/samagotchi/web/public/index.html +25 -20
  81. data/lib/samagotchi/web/public/kept_commands.js +52 -0
  82. data/lib/samagotchi/web/public/model_pick.js +3 -1
  83. data/lib/samagotchi/web/public/session_title.js +26 -0
  84. data/lib/samagotchi/web/public/strip.js +4 -6
  85. data/lib/samagotchi/web/public/thinking_chip.js +169 -0
  86. data/lib/samagotchi/web/public/timing.js +15 -0
  87. data/lib/samagotchi/web/public/turn_events.js +26 -2
  88. data/lib/samagotchi/web/public/turn_html.js +3 -1
  89. data/lib/samagotchi/web/public/turn_view.js +328 -310
  90. data/lib/samagotchi/web/public/vendor/lit-html.js +8 -0
  91. data/lib/samagotchi/web/session_summary.rb +1 -1
  92. data/lib/samagotchi/worker.rb +44 -43
  93. data/lib/samagotchi/worker_inbound.rb +10 -7
  94. data/lib/samagotchi/worker_wakes.rb +0 -2
  95. data/lib/samagotchi/yaml_lines.rb +72 -0
  96. data/lib/samagotchi.rb +1 -0
  97. metadata +13 -1
@@ -10,6 +10,7 @@ require "rack"
10
10
  require "rack/request"
11
11
 
12
12
  require_relative "../client_id"
13
+ require_relative "../delivery"
13
14
  require_relative "../answer_tail"
14
15
  require_relative "../bridge_client"
15
16
  require_relative "../bridge/bounded_queue"
@@ -23,6 +24,7 @@ require_relative "../host_registry"
23
24
  require_relative "../llm_context_override"
24
25
  require_relative "../model_catalog"
25
26
  require_relative "../model_profile"
27
+ require_relative "../thinking"
26
28
  require_relative "../sampling_settings"
27
29
  require_relative "../served_model"
28
30
  require_relative "../project_scope"
@@ -476,6 +478,7 @@ module Samagotchi
476
478
  summary = SamplingSettings.summary(target, names: names, models: settings)
477
479
  model[:sampling] = summary if summary
478
480
  add_llm_context(model, target, names, settings)
481
+ add_thinking(model, target, names, settings)
479
482
  end
480
483
  rescue StandardError
481
484
  nil
@@ -489,6 +492,14 @@ module Samagotchi
489
492
  nil
490
493
  end
491
494
 
495
+ # The thinking level +model+ starts at, with where it came from (the
496
+ # start page's think chip; Thinking::Explained#summary, no session).
497
+ def add_thinking(model, target, names, settings)
498
+ model[:thinking] = Thinking.explain(target, names: names, models: settings).summary
499
+ rescue StandardError
500
+ nil
501
+ end
502
+
492
503
  # The served model a session's last generation reported (analytics.json,
493
504
  # SessionMetrics#persist), for a session no worker runs: shown only
494
505
  # while the name it answered is still the session's model, as
@@ -525,6 +536,19 @@ module Samagotchi
525
536
  LLMContextStrategy.for_session(session, registry: host_registry)
526
537
  end
527
538
 
539
+ # A session's thinking level with where it came from and its own
540
+ # (Thinking::Explained#summary), as its worker would resolve it; nil
541
+ # when it can't be. Config and the registry only: no network.
542
+ def thinking_for(session)
543
+ registry = host_registry
544
+ name = session.model_name.to_s.strip.empty? ? ModelProfile.required_model_name(nil) : session.model_name
545
+ target = registry.resolve(name)
546
+ names = registry.lookup_names(session.model_typed || name, resolved: name, target: target)
547
+ Thinking.explain(target, names: names, session: session.thinking).summary
548
+ rescue StandardError
549
+ nil
550
+ end
551
+
528
552
  # Built again when config.yml's hosts: changed (compared as read: the
529
553
  # YAML read is mtime-cached), so the model picker lists a host added
530
554
  # while chi web runs. An injected registry (specs) stays.
@@ -607,6 +631,11 @@ module Samagotchi
607
631
  llm_context, error = request_llm_context(body["llm_context"])
608
632
  return error if error
609
633
 
634
+ # thinking: the session's own thinking level (the start page's think
635
+ # chip; chi --thinking), checked the same way.
636
+ thinking, error = request_thinking(body["thinking"])
637
+ return error if error
638
+
610
639
  folder = dir ? { working_directory: dir } : {}
611
640
  # model: the model the session starts on (the start page's picker,
612
641
  # spelled as GET /api/models lists it); blank means the default.
@@ -617,7 +646,8 @@ module Samagotchi
617
646
  # until that turn is saved.
618
647
  preview = idle ? body["preview"].to_s.strip : ""
619
648
  folder[:title] = preview unless preview.empty?
620
- folder[:llm_context] = llm_context if llm_context
649
+ setup = SessionSetup.new(llm_context: llm_context, thinking: thinking)
650
+ folder[:setup] = setup unless setup.empty?
621
651
  begin
622
652
  session = @manager.spawn_session(prompt: idle ? nil : prompt.to_s, state_dir: @state_dir, **folder)
623
653
  rescue ModelProfile::MissingModel => e
@@ -635,8 +665,8 @@ module Samagotchi
635
665
 
636
666
  # POST /api/sessions with continues: <id, prefix or last:id>: the next
637
667
  # link of that session's chain (SessionManager.continue_session), in its
638
- # folder, on its model and with its llm_context, so dir, model and
639
- # llm_context are refused beside it. 409 continued (with next_id, the
668
+ # folder, on its model and with its llm_context and thinking level, so
669
+ # dir, model, llm_context and thinking are refused beside it. 409 continued (with next_id, the
640
670
  # link that continues it already: the page opens that one),
641
671
  # open_children (ids), folder_gone, busy, scratch or owned_by_tui.
642
672
  def handle_continue(body, prompt, idle)
@@ -644,9 +674,9 @@ module Samagotchi
644
674
  unless given.is_a?(String) && !given.strip.empty?
645
675
  return error_response(400, "invalid_continues", "continues is a session id")
646
676
  end
647
- if (fields = %w[dir model model_name llm_context].select { |key| body.key?(key) }).any?
677
+ if (fields = %w[dir model model_name llm_context thinking].select { |key| body.key?(key) }).any?
648
678
  return error_response(400, "invalid_continues",
649
- "continues takes the previous session's folder, model and llm_context; " \
679
+ "continues takes the previous session's folder, model, llm_context and thinking; " \
650
680
  "leave out #{fields.join(", ")}")
651
681
  end
652
682
 
@@ -701,6 +731,20 @@ module Samagotchi
701
731
  [nil, error_response(400, "invalid_llm_context", e.message)]
702
732
  end
703
733
 
734
+ # The create request's "thinking" (a level word as /thinking takes it,
735
+ # "default" for none) as the session's own level: [level, nil] (nil
736
+ # when absent or default), or [nil, a 400 response] for anything else.
737
+ def request_thinking(raw)
738
+ return [nil, nil] if raw.nil?
739
+
740
+ word = raw.is_a?(String) ? raw.strip.downcase : nil
741
+ unless word && Thinking::LEVELS.map(&:to_s).include?(word)
742
+ return [nil, error_response(400, "invalid_thinking", "thinking is one of #{Thinking::LEVELS.join(", ")}")]
743
+ end
744
+
745
+ [Thinking.session_level(word), nil]
746
+ end
747
+
704
748
  def handle_show(req, id)
705
749
  # The page's light re-reads: never the whole conversation.
706
750
  return handle_cards_read(id) if req.params["cards"] == "1"
@@ -750,6 +794,8 @@ module Samagotchi
750
794
  # The LLM context strategy the next turn runs under (the info bar's
751
795
  # chip): the worker's, else worked out here from the session file.
752
796
  session_json = session_json.merge(llm_context: (snapshot && snapshot["llm_context"]) || llm_context_for(session))
797
+ # The thinking level the next turn runs at (the think chip), the same way.
798
+ session_json = session_json.merge(thinking: (snapshot && snapshot["thinking"]) || thinking_for(session))
753
799
  raw_messages = turn_snapshot ? turn_snapshot["messages"] : session.messages
754
800
  timing = timing_payload(id, live_metrics: snapshot && snapshot["metrics"], persisted: persisted)
755
801
  json_response(200, {
@@ -1065,8 +1111,14 @@ module Samagotchi
1065
1111
  images = turn_images(id, body["images"])
1066
1112
  return error_response(400, "bad_images", images) if images.is_a?(String)
1067
1113
 
1068
- result = SessionManager.deliver_turn(id, prompt: prompt.to_s, client_id: client_id, images: images, state_dir: @state_dir,
1069
- manager: @manager, bridge: -> { live_bridge_client(id) })
1114
+ delivery = body["delivery"] || body[:delivery]
1115
+ if (refused = Delivery.refusal(delivery))
1116
+ return error_response(400, refused[:error], refused[:detail])
1117
+ end
1118
+
1119
+ result = SessionManager.deliver_turn(id, prompt: prompt.to_s, client_id: client_id, images: images,
1120
+ delivery: delivery, state_dir: @state_dir, manager: @manager,
1121
+ bridge: -> { live_bridge_client(id) })
1070
1122
  case result[:status]
1071
1123
  when :accepted
1072
1124
  record_history(body, prompt)
@@ -233,6 +233,9 @@ module Samagotchi
233
233
  part = { tool: name, params: params.to_s }
234
234
  title = ToolActivity.tool_title(name, call, cwd: cwd)
235
235
  part[:title] = title if title
236
+ # The name the model called it by when that was an alias's (the
237
+ # saved call is rebuilt by BuiltinCalls, which sets it again).
238
+ part[:called_as] = call[:called_as] if call[:called_as]
236
239
  # Built from the saved call (never saved itself), so older sessions
237
240
  # get it too.
238
241
  view = ToolView.for(name, call)&.to_h
@@ -53,6 +53,7 @@ export function addStarted(model, event = {}, now = null) {
53
53
  row.label = event.label || row.label;
54
54
  row.params = event.params || row.params;
55
55
  row.title = event.title || row.title;
56
+ row.called_as = event.called_as || row.called_as;
56
57
  row.view = event.view || row.view;
57
58
  markStart(row, event, now);
58
59
  return { row, created: false };
@@ -68,6 +69,9 @@ export function addStarted(model, event = {}, now = null) {
68
69
  // project-relative path, a command without its "cd … &&"); rows show it
69
70
  // in place of the params when there is one.
70
71
  title: event.title || null,
72
+ // The name the model called the tool by when it was an alias's ("bash"
73
+ // run as execute); the row says so.
74
+ called_as: event.called_as || null,
71
75
  // The call's full arguments for the expanded row and the hovers (the
72
76
  // server's ToolView: an execute/task_create's command, its cwd).
73
77
  view: event.view || null,
@@ -104,6 +108,7 @@ export function addCompleted(model, event = {}, now = null) {
104
108
  label: event.label,
105
109
  params: event.activity?.params || "",
106
110
  title: event.activity?.title,
111
+ called_as: event.activity?.called_as,
107
112
  view: event.view,
108
113
  });
109
114
  row = synthesized.row;
@@ -113,6 +118,7 @@ export function addCompleted(model, event = {}, now = null) {
113
118
  row.output_truncated = !!event.output_truncated;
114
119
  if (event.activity?.params) row.params = event.activity.params;
115
120
  if (event.activity?.title) row.title = event.activity.title;
121
+ if (event.activity?.called_as) row.called_as = event.activity.called_as;
116
122
  // The images the tool returned (a reload in the turn view draws them from
117
123
  // the saved parts, turn_html.js toolRowHtml).
118
124
  if (event.images?.length) row.images = event.images;