samagotchi 0.51.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -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 +23 -8
  7. data/docs/configuration.md +59 -19
  8. data/docs/memory.md +11 -0
  9. data/docs/plugins.md +2 -1
  10. data/docs/sessions.md +5 -1
  11. data/docs/testing.md +6 -1
  12. data/lib/samagotchi/bootstrap/config_writer.rb +6 -37
  13. data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
  14. data/lib/samagotchi/bridge.rb +10 -13
  15. data/lib/samagotchi/broadcast/recipients.rb +1 -2
  16. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  17. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  18. data/lib/samagotchi/children_status.rb +1 -1
  19. data/lib/samagotchi/commands/registry.rb +2 -1
  20. data/lib/samagotchi/config.rb +50 -182
  21. data/lib/samagotchi/config_text_edit.rb +8 -32
  22. data/lib/samagotchi/context_sources.rb +0 -7
  23. data/lib/samagotchi/context_window.rb +2 -2
  24. data/lib/samagotchi/continue_offer.rb +5 -4
  25. data/lib/samagotchi/cut_policy.rb +0 -1
  26. data/lib/samagotchi/delivery.rb +23 -0
  27. data/lib/samagotchi/engine.rb +48 -17
  28. data/lib/samagotchi/engine_peers.rb +1 -0
  29. data/lib/samagotchi/host_config.rb +183 -0
  30. data/lib/samagotchi/host_registry.rb +5 -10
  31. data/lib/samagotchi/last_turn.rb +48 -0
  32. data/lib/samagotchi/live_versions.rb +0 -6
  33. data/lib/samagotchi/llm/native_tool_normalizer.rb +0 -5
  34. data/lib/samagotchi/llm_context_override.rb +3 -22
  35. data/lib/samagotchi/llm_context_strategy.rb +50 -16
  36. data/lib/samagotchi/memory_bundle/placeholder.rb +0 -5
  37. data/lib/samagotchi/model_profile.rb +4 -4
  38. data/lib/samagotchi/model_settings.rb +46 -0
  39. data/lib/samagotchi/plugin/context.rb +3 -3
  40. data/lib/samagotchi/plugin/sessions.rb +2 -2
  41. data/lib/samagotchi/queued_command.rb +35 -0
  42. data/lib/samagotchi/reminder_store.rb +0 -27
  43. data/lib/samagotchi/reply_wait.rb +11 -13
  44. data/lib/samagotchi/sampling_settings.rb +4 -4
  45. data/lib/samagotchi/self_report.rb +8 -5
  46. data/lib/samagotchi/send_command.rb +37 -19
  47. data/lib/samagotchi/session.rb +45 -24
  48. data/lib/samagotchi/session_commands.rb +36 -7
  49. data/lib/samagotchi/session_inbox.rb +18 -9
  50. data/lib/samagotchi/session_manager.rb +8 -7
  51. data/lib/samagotchi/session_setup.rb +30 -0
  52. data/lib/samagotchi/steer_cut.rb +9 -2
  53. data/lib/samagotchi/terminal_ui/attach_launcher.rb +10 -4
  54. data/lib/samagotchi/terminal_ui/attached_loop.rb +1 -1
  55. data/lib/samagotchi/terminal_ui/formatting.rb +21 -3
  56. data/lib/samagotchi/terminal_ui.rb +8 -3
  57. data/lib/samagotchi/thinking.rb +67 -7
  58. data/lib/samagotchi/thinking_command.rb +72 -0
  59. data/lib/samagotchi/tools/builtins.rb +7 -2
  60. data/lib/samagotchi/tools/peers.rb +3 -2
  61. data/lib/samagotchi/tools/task_runtime.rb +1 -1
  62. data/lib/samagotchi/version.rb +1 -1
  63. data/lib/samagotchi/vision_support.rb +4 -3
  64. data/lib/samagotchi/web/app.rb +54 -6
  65. data/lib/samagotchi/web/public/app.js +133 -20
  66. data/lib/samagotchi/web/public/data.js +8 -4
  67. data/lib/samagotchi/web/public/format.js +8 -4
  68. data/lib/samagotchi/web/public/index.html +8 -5
  69. data/lib/samagotchi/web/public/kept_commands.js +52 -0
  70. data/lib/samagotchi/web/public/model_pick.js +3 -1
  71. data/lib/samagotchi/web/public/thinking_chip.js +169 -0
  72. data/lib/samagotchi/web/public/timing.js +15 -0
  73. data/lib/samagotchi/web/public/turn_events.js +23 -0
  74. data/lib/samagotchi/web/public/turn_view.js +328 -310
  75. data/lib/samagotchi/web/public/vendor/lit-html.js +8 -0
  76. data/lib/samagotchi/web/session_summary.rb +1 -1
  77. data/lib/samagotchi/worker.rb +43 -42
  78. data/lib/samagotchi/worker_inbound.rb +5 -5
  79. data/lib/samagotchi/worker_wakes.rb +0 -2
  80. data/lib/samagotchi/yaml_lines.rb +72 -0
  81. metadata +11 -1
@@ -0,0 +1,8 @@
1
+ // lit-html 3.3.3 (npm tarball lit-html-3.3.3.tgz, lit-html.js: the minified production
2
+ // build, unmodified but for this line and the dropped sourceMappingURL). BSD-3-Clause.
3
+ /**
4
+ * @license
5
+ * Copyright 2017 Google LLC
6
+ * SPDX-License-Identifier: BSD-3-Clause
7
+ */
8
+ const t=globalThis,i=t=>t,s=t.trustedTypes,e=s?s.createPolicy("lit-html",{createHTML:t=>t}):void 0,h="$lit$",o=`lit$${Math.random().toFixed(9).slice(2)}$`,n="?"+o,r=`<${n}>`,l=document,c=()=>l.createComment(""),a=t=>null===t||"object"!=typeof t&&"function"!=typeof t,u=Array.isArray,d=t=>u(t)||"function"==typeof t?.[Symbol.iterator],f="[ \t\n\f\r]",v=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,_=/-->/g,m=/>/g,p=RegExp(`>|${f}(?:([^\\s"'>=/]+)(${f}*=${f}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),g=/'/g,$=/"/g,y=/^(?:script|style|textarea|title)$/i,x=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),b=x(1),w=x(2),T=x(3),E=Symbol.for("lit-noChange"),A=Symbol.for("lit-nothing"),C=new WeakMap,P=l.createTreeWalker(l,129);function V(t,i){if(!u(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==e?e.createHTML(i):i}const N=(t,i)=>{const s=t.length-1,e=[];let n,l=2===i?"<svg>":3===i?"<math>":"",c=v;for(let i=0;i<s;i++){const s=t[i];let a,u,d=-1,f=0;for(;f<s.length&&(c.lastIndex=f,u=c.exec(s),null!==u);)f=c.lastIndex,c===v?"!--"===u[1]?c=_:void 0!==u[1]?c=m:void 0!==u[2]?(y.test(u[2])&&(n=RegExp("</"+u[2],"g")),c=p):void 0!==u[3]&&(c=p):c===p?">"===u[0]?(c=n??v,d=-1):void 0===u[1]?d=-2:(d=c.lastIndex-u[2].length,a=u[1],c=void 0===u[3]?p:'"'===u[3]?$:g):c===$||c===g?c=p:c===_||c===m?c=v:(c=p,n=void 0);const x=c===p&&t[i+1].startsWith("/>")?" ":"";l+=c===v?s+r:d>=0?(e.push(a),s.slice(0,d)+h+s.slice(d)+o+x):s+o+(-2===d?i:x)}return[V(t,l+(t[s]||"<?>")+(2===i?"</svg>":3===i?"</math>":"")),e]};class S{constructor({strings:t,_$litType$:i},e){let r;this.parts=[];let l=0,a=0;const u=t.length-1,d=this.parts,[f,v]=N(t,i);if(this.el=S.createElement(f,e),P.currentNode=this.el.content,2===i||3===i){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(r=P.nextNode())&&d.length<u;){if(1===r.nodeType){if(r.hasAttributes())for(const t of r.getAttributeNames())if(t.endsWith(h)){const i=v[a++],s=r.getAttribute(t).split(o),e=/([.?@])?(.*)/.exec(i);d.push({type:1,index:l,name:e[2],strings:s,ctor:"."===e[1]?I:"?"===e[1]?L:"@"===e[1]?z:H}),r.removeAttribute(t)}else t.startsWith(o)&&(d.push({type:6,index:l}),r.removeAttribute(t));if(y.test(r.tagName)){const t=r.textContent.split(o),i=t.length-1;if(i>0){r.textContent=s?s.emptyScript:"";for(let s=0;s<i;s++)r.append(t[s],c()),P.nextNode(),d.push({type:2,index:++l});r.append(t[i],c())}}}else if(8===r.nodeType)if(r.data===n)d.push({type:2,index:l});else{let t=-1;for(;-1!==(t=r.data.indexOf(o,t+1));)d.push({type:7,index:l}),t+=o.length-1}l++}}static createElement(t,i){const s=l.createElement("template");return s.innerHTML=t,s}}function M(t,i,s=t,e){if(i===E)return i;let h=void 0!==e?s._$Co?.[e]:s._$Cl;const o=a(i)?void 0:i._$litDirective$;return h?.constructor!==o&&(h?._$AO?.(!1),void 0===o?h=void 0:(h=new o(t),h._$AT(t,s,e)),void 0!==e?(s._$Co??=[])[e]=h:s._$Cl=h),void 0!==h&&(i=M(t,h._$AS(t,i.values),h,e)),i}class R{constructor(t,i){this._$AV=[],this._$AN=void 0,this._$AD=t,this._$AM=i}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(t){const{el:{content:i},parts:s}=this._$AD,e=(t?.creationScope??l).importNode(i,!0);P.currentNode=e;let h=P.nextNode(),o=0,n=0,r=s[0];for(;void 0!==r;){if(o===r.index){let i;2===r.type?i=new k(h,h.nextSibling,this,t):1===r.type?i=new r.ctor(h,r.name,r.strings,this,t):6===r.type&&(i=new Z(h,this,t)),this._$AV.push(i),r=s[++n]}o!==r?.index&&(h=P.nextNode(),o++)}return P.currentNode=l,e}p(t){let i=0;for(const s of this._$AV)void 0!==s&&(void 0!==s.strings?(s._$AI(t,s,i),i+=s.strings.length-2):s._$AI(t[i])),i++}}class k{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(t,i,s,e){this.type=2,this._$AH=A,this._$AN=void 0,this._$AA=t,this._$AB=i,this._$AM=s,this.options=e,this._$Cv=e?.isConnected??!0}get parentNode(){let t=this._$AA.parentNode;const i=this._$AM;return void 0!==i&&11===t?.nodeType&&(t=i.parentNode),t}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(t,i=this){t=M(this,t,i),a(t)?t===A||null==t||""===t?(this._$AH!==A&&this._$AR(),this._$AH=A):t!==this._$AH&&t!==E&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):d(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==A&&a(this._$AH)?this._$AA.nextSibling.data=t:this.T(l.createTextNode(t)),this._$AH=t}$(t){const{values:i,_$litType$:s}=t,e="number"==typeof s?this._$AC(t):(void 0===s.el&&(s.el=S.createElement(V(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===e)this._$AH.p(i);else{const t=new R(e,this),s=t.u(this.options);t.p(i),this.T(s),this._$AH=t}}_$AC(t){let i=C.get(t.strings);return void 0===i&&C.set(t.strings,i=new S(t)),i}k(t){u(this._$AH)||(this._$AH=[],this._$AR());const i=this._$AH;let s,e=0;for(const h of t)e===i.length?i.push(s=new k(this.O(c()),this.O(c()),this,this.options)):s=i[e],s._$AI(h),e++;e<i.length&&(this._$AR(s&&s._$AB.nextSibling,e),i.length=e)}_$AR(t=this._$AA.nextSibling,s){for(this._$AP?.(!1,!0,s);t!==this._$AB;){const s=i(t).nextSibling;i(t).remove(),t=s}}setConnected(t){void 0===this._$AM&&(this._$Cv=t,this._$AP?.(t))}}class H{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(t,i,s,e,h){this.type=1,this._$AH=A,this._$AN=void 0,this.element=t,this.name=i,this._$AM=e,this.options=h,s.length>2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=A}_$AI(t,i=this,s,e){const h=this.strings;let o=!1;if(void 0===h)t=M(this,t,i,0),o=!a(t)||t!==this._$AH&&t!==E,o&&(this._$AH=t);else{const e=t;let n,r;for(t=h[0],n=0;n<h.length-1;n++)r=M(this,e[s+n],i,n),r===E&&(r=this._$AH[n]),o||=!a(r)||r!==this._$AH[n],r===A?t=A:t!==A&&(t+=(r??"")+h[n+1]),this._$AH[n]=r}o&&!e&&this.j(t)}j(t){t===A?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,t??"")}}class I extends H{constructor(){super(...arguments),this.type=3}j(t){this.element[this.name]=t===A?void 0:t}}class L extends H{constructor(){super(...arguments),this.type=4}j(t){this.element.toggleAttribute(this.name,!!t&&t!==A)}}class z extends H{constructor(t,i,s,e,h){super(t,i,s,e,h),this.type=5}_$AI(t,i=this){if((t=M(this,t,i,0)??A)===E)return;const s=this._$AH,e=t===A&&s!==A||t.capture!==s.capture||t.once!==s.once||t.passive!==s.passive,h=t!==A&&(s===A||e);e&&this.element.removeEventListener(this.name,this,s),h&&this.element.addEventListener(this.name,this,t),this._$AH=t}handleEvent(t){"function"==typeof this._$AH?this._$AH.call(this.options?.host??this.element,t):this._$AH.handleEvent(t)}}class Z{constructor(t,i,s){this.element=t,this.type=6,this._$AN=void 0,this._$AM=i,this.options=s}get _$AU(){return this._$AM._$AU}_$AI(t){M(this,t)}}const j={M:h,P:o,A:n,C:1,L:N,R,D:d,V:M,I:k,H,N:L,U:z,B:I,F:Z},B=t.litHtmlPolyfillSupport;B?.(S,k),(t.litHtmlVersions??=[]).push("3.3.3");const D=(t,i,s)=>{const e=s?.renderBefore??i;let h=e._$litPart$;if(void 0===h){const t=s?.renderBefore??null;e._$litPart$=h=new k(i.insertBefore(c(),t),t,void 0,s??{})}return h._$AI(t),h};export{j as _$LH,b as html,T as mathml,E as noChange,A as nothing,D as render,w as svg};
@@ -93,7 +93,7 @@ module Samagotchi
93
93
  # chi REPL's (SessionManager.worker_live?'s rule: only a worker shares it).
94
94
  pending_question: session.waiting_question(live: !!owner&.worker?),
95
95
  pending_card: up ? Bridge::PendingCard.read(session_dir) : nil,
96
- last_turn: session.last_turn
96
+ last_turn: session.last_turn&.to_file
97
97
  }
98
98
  end
99
99
 
@@ -20,6 +20,7 @@ require_relative "llm/errors"
20
20
  require_relative "continue_offer"
21
21
  require_relative "iteration_limit"
22
22
  require_relative "session_commands"
23
+ require_relative "queued_command"
23
24
  require_relative "model_profile"
24
25
  require_relative "child_reports"
25
26
  require_relative "child_move"
@@ -175,9 +176,12 @@ module Samagotchi
175
176
  # stream gets the session's history and status in its snapshot, not
176
177
  # an empty session until the first turn.
177
178
  @engine.session = @session
178
- # Input already waiting in the inbox starts the next turn at once: no
179
- # turn-end warm-up then.
180
- @engine.next_turn_waiting = -> { SessionInbox.find_new_input_files(@session_dir).any? || wake_due? }
179
+ # Input already waiting in the inbox starts the next turn at once, and
180
+ # a command queued mid-turn (/thinking, /model, /llm-context) changes
181
+ # what it starts with: no turn-end warm-up then.
182
+ @engine.next_turn_waiting = lambda {
183
+ !@command_queue.empty? || SessionInbox.find_new_input_files(@session_dir).any? || wake_due?
184
+ }
181
185
  @turn_flow = TurnFlow.new(engine: @engine)
182
186
  # Its question's answer is queued as the matching /continue (the
183
187
  # Bridge exists by the time anyone answers).
@@ -592,9 +596,9 @@ module Samagotchi
592
596
  end
593
597
 
594
598
  def ready_command?(command)
595
- return true if command[:after_file].nil?
599
+ return true if command.after_file.nil?
596
600
 
597
- SessionInbox.find_new_input_files(@session_dir).none? { |path| File.basename(path) <= command[:after_file] }
601
+ SessionInbox.find_new_input_files(@session_dir).none? { |path| File.basename(path) <= command.after_file }
598
602
  end
599
603
 
600
604
  def drain_commands
@@ -619,11 +623,11 @@ module Samagotchi
619
623
 
620
624
  @engine.synchronize_events do
621
625
  drain_commands.each do |command|
622
- during = mid_turn || command[:after_seq].to_i < @turn_end_seq
626
+ during = mid_turn || command.after_seq.to_i < @turn_end_seq
623
627
  case (during ? turn_answer(command, ended: !mid_turn) : :keep)
624
628
  when :refuse then announce_command(command, status: "busy", output: busy_output(command), changed: [])
625
629
  when :drop then drop_command(waiting_for_turn_end(command), "turn #{@turn_end_type == :turn_failed ? "failed" : "canceled"}: " \
626
- "#{command[:line]} not run; send it again")
630
+ "#{command.line} not run; send it again")
627
631
  else @command_queue << (during ? waiting_for_turn_end(command) : command)
628
632
  end
629
633
  end
@@ -634,8 +638,8 @@ module Samagotchi
634
638
  # (D4, once it +ended+) or :keep. One queued idle just as the turn
635
639
  # began (:loop) goes by its line's policy (a show form waits too).
636
640
  def turn_answer(command, ended:)
637
- policy = command[:mid_turn]
638
- policy = @engine.command_registry.mid_turn(command[:line]) if policy == :loop
641
+ policy = command.mid_turn
642
+ policy = @engine.command_registry.mid_turn(command.line) if policy == :loop
639
643
  return :refuse if policy == :refuse
640
644
  return :drop if ended && policy == :queue && shell_command?(command) &&
641
645
  %i[turn_canceled turn_failed].include?(@turn_end_type)
@@ -648,18 +652,16 @@ module Samagotchi
648
652
  # marked as one queued mid-turn: its command_queued (waits) tells the
649
653
  # UIs now, and its command_ran says queued.
650
654
  def waiting_for_turn_end(command)
651
- return command unless command[:mid_turn] == :loop && @engine.command_registry.mid_turn(command[:line]) == :queue
655
+ return command unless command.mid_turn == :loop && @engine.command_registry.mid_turn(command.line) == :queue
652
656
 
653
- event = { type: :command_queued, **command.slice(:command_id, :client_id, :line), waits: "turn_end" }
654
- event[:card] = true if command[:card]
655
- @engine.announce(event)
656
- command.merge(mid_turn: :queue)
657
+ @engine.announce(type: :command_queued, **command.event_fields, waits: "turn_end")
658
+ command.with(mid_turn: :queue)
657
659
  end
658
660
 
659
- def shell_command?(command) = @engine.command_registry.lookup(command[:line])&.id == :shell
661
+ def shell_command?(command) = @engine.command_registry.lookup(command.line)&.id == :shell
660
662
 
661
663
  def busy_output(command)
662
- @engine.command_registry.lookup(command[:line])&.id == :rollback ? ROLLBACK_BUSY_OUTPUT : BUSY_OUTPUT
664
+ @engine.command_registry.lookup(command.line)&.id == :rollback ? ROLLBACK_BUSY_OUTPUT : BUSY_OUTPUT
663
665
  end
664
666
 
665
667
  # +command+ won't run: its sender (a UI's queued bubble) is told.
@@ -695,7 +697,7 @@ module Samagotchi
695
697
  shown.each { |event| @engine.announce(event) }
696
698
  end
697
699
  save_or_log(:command) { save_session } unless Array(result.changed).empty?
698
- user_input(command[:client_id]) if resolved
700
+ user_input(command.client_id) if resolved
699
701
  @continue_offer.after_command(result, resolved: resolved)
700
702
  @continue_offer.run_continue_turn(command) if result.resume
701
703
  end
@@ -704,12 +706,16 @@ module Samagotchi
704
706
  # the cards and notices it showed, held: they follow its command_ran,
705
707
  # as its output
706
708
  def run_command_line(command)
707
- result, shown = @engine.holding_announcements do
708
- @commands.run(command[:line])
709
- rescue StandardError => e
710
- SessionCommands::Result.new(status: :error, output: "#{command[:line].split.first}: #{e.message}", changed: [])
711
- end
712
- [result || SessionCommands::Result.new(status: :error, output: "not a session command", changed: []), shown]
709
+ @engine.holding_announcements { command_result(command) { @commands.run(command.line) } }
710
+ end
711
+
712
+ # The block's Result for +command+; one that raised, or found no
713
+ # command for the line, is an error Result saying so.
714
+ # @return [SessionCommands::Result]
715
+ def command_result(command)
716
+ yield || SessionCommands::Result.new(status: :error, output: "not a session command", changed: [])
717
+ rescue StandardError => e
718
+ SessionCommands::Result.new(status: :error, output: "#{command.line.split.first}: #{e.message}", changed: [])
713
719
  end
714
720
 
715
721
  # A session command from the Bridge, called with the event log held:
@@ -731,8 +737,8 @@ module Samagotchi
731
737
  # :refuse while a turn runs, :loop idle
732
738
  def on_command(command, after_file: nil)
733
739
  registry = @engine.command_registry
734
- policy = if @engine.turn_running? then registry.mid_turn(command[:line])
735
- else registry.anytime?(command[:line]) ? :anytime : :loop
740
+ policy = if @engine.turn_running? then registry.mid_turn(command.line)
741
+ else registry.anytime?(command.line) ? :anytime : :loop
736
742
  end
737
743
  if policy == :anytime
738
744
  start_anytime_command(command)
@@ -741,7 +747,7 @@ module Samagotchi
741
747
 
742
748
  after_file ||= newest_input_file if policy == :queue
743
749
  # The count says which turn ends came before it.
744
- @command_queue << command.merge(after_seq: @engine.event_count, mid_turn: policy, after_file: after_file).compact
750
+ @command_queue << command.with(after_seq: @engine.event_count, mid_turn: policy, after_file: after_file)
745
751
  @waker.wake
746
752
  policy
747
753
  end
@@ -761,16 +767,11 @@ module Samagotchi
761
767
  # saved.
762
768
  def start_anytime_command(command)
763
769
  @engine.spawn_anytime do
764
- result = begin
765
- @engine.running_anytime { @commands.run(command[:line]) }
766
- rescue StandardError => e
767
- SessionCommands::Result.new(status: :error, output: "#{command[:line].split.first}: #{e.message}", changed: [])
768
- end
769
- result ||= SessionCommands::Result.new(status: :error, output: "not a session command", changed: [])
770
+ result = command_result(command) { @engine.running_anytime { @commands.run(command.line) } }
770
771
  announce_command(command, status: result.status.to_s, output: result.output, changed: Array(result.changed),
771
772
  anytime: true)
772
773
  rescue StandardError => e
773
- Log.warn(:worker, "anytime_command_failed", line: command[:line], error: e.class.name, msg: e.message)
774
+ Log.warn(:worker, "anytime_command_failed", line: command.line, error: e.class.name, msg: e.message)
774
775
  end
775
776
  end
776
777
 
@@ -778,15 +779,15 @@ module Samagotchi
778
779
  # its command_queued already
779
780
  def announce_command(command, status:, output:, changed:, anytime: false)
780
781
  text = output.to_s
781
- event = { type: :command_ran, command_id: command[:command_id], client_id: command[:client_id], line: command[:line],
782
- status: status, output: text[0, COMMAND_OUTPUT_LIMIT], changed: changed.map(&:to_s),
783
- model_name: @engine.effective_model_name }
782
+ event = { type: :command_ran, **command.event_fields, status: status, output: text[0, COMMAND_OUTPUT_LIMIT],
783
+ changed: changed.map(&:to_s), model_name: @engine.effective_model_name }
784
784
  event[:anytime] = true if anytime
785
785
  # Its line was shown at its command_queued (waits: turn_end).
786
- event[:queued] = true if command[:mid_turn] == :queue
787
- event[:card] = true if command[:card]
786
+ event[:queued] = true if command.waits_for_turn_end?
788
787
  # /llm-context or /model: what the next turn runs under now (the web's chip).
789
788
  event[:llm_context] = @engine.llm_context_summary if changed.intersect?(%i[llm_context model])
789
+ # /thinking or /model: the level the next turn runs at (the web's chip).
790
+ event[:thinking] = @engine.thinking_explained&.summary if changed.intersect?(%i[thinking model])
790
791
  event[:output_truncated] = true if text.length > COMMAND_OUTPUT_LIMIT
791
792
  @engine.announce(event)
792
793
  end
@@ -867,7 +868,7 @@ module Samagotchi
867
868
  def merges_lost?(error)
868
869
  return false if error.nil? || error.is_a?(LLM::FailedTurn)
869
870
 
870
- !KEPT_TURN_OUTCOMES.include?(@session.last_turn&.dig("outcome"))
871
+ !KEPT_TURN_OUTCOMES.include?(@session.last_turn&.outcome)
871
872
  end
872
873
 
873
874
  # The Engine's turn endings that keep the turn's conversation (#merges_lost?).
@@ -959,9 +960,9 @@ module Samagotchi
959
960
  next unless claimed_file
960
961
 
961
962
  begin
962
- prompt, origin = SessionInbox.read_input(claimed_file)
963
- prompt = prompt.to_s.strip
964
- prompt.empty? ? nil : [prompt, origin]
963
+ input = SessionInbox.read_input(claimed_file)
964
+ prompt = input.prompt.to_s.strip
965
+ prompt.empty? ? nil : [prompt, input.origin]
965
966
  ensure
966
967
  FileUtils.rm_f(claimed_file)
967
968
  end
@@ -183,12 +183,12 @@ module Samagotchi
183
183
  return unless claimed_file
184
184
 
185
185
  begin
186
- message, origin, no_interrupt, images, delivery = SessionInbox.read_input(claimed_file)
187
- return if message.to_s.strip.empty?
188
- return if Array(images).empty? && queue_as_command(message, origin, File.basename(input_file))
186
+ input = SessionInbox.read_input(claimed_file)
187
+ return if input.prompt.to_s.strip.empty?
188
+ return if input.images.empty? && queue_as_command(input.prompt, input.origin, File.basename(input_file))
189
189
 
190
- yield Prompt.new(text: message, origin: origin, no_interrupt: !!no_interrupt, images: images || [],
191
- delivery: delivery)
190
+ yield Prompt.new(text: input.prompt, origin: input.origin, no_interrupt: input.no_interrupt, images: input.images,
191
+ delivery: input.delivery)
192
192
  ensure
193
193
  FileUtils.rm_f(claimed_file)
194
194
  end
@@ -31,8 +31,6 @@ module Samagotchi
31
31
  # Wake turns run since the last human input.
32
32
  attr_reader :in_a_row
33
33
 
34
- def paused? = @paused
35
-
36
34
  def grace_left = @grace - (@clock.call - @started_at)
37
35
 
38
36
  def in_grace? = grace_left.positive?
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "psych"
5
+
6
+ module Samagotchi
7
+ # Line helpers for editing a YAML file's text in place, so the user's
8
+ # comments and layout stay (YAML.dump would drop them): where a top-level
9
+ # block ends, where a new line goes in it, and whether the text holds
10
+ # anything a line edit can't handle (anchors and aliases). Shared by
11
+ # ConfigTextEdit (one section.key) and Bootstrap::ConfigWriter (a hosts:
12
+ # entry).
13
+ module YAMLLines
14
+ DEFAULT_INDENT = 2
15
+
16
+ module_function
17
+
18
+ # +text+ as its lines (each keeping its line end, the last one given
19
+ # one) and the line end it uses.
20
+ # @return [Array(Array<String>, String)]
21
+ def lines_of(text)
22
+ eol = text.include?("\r\n") ? "\r\n" : "\n"
23
+ lines = text.split(/(?<=\n)/)
24
+ lines[-1] = "#{lines[-1]}#{eol}" if lines.any? && !lines[-1].end_with?("\n")
25
+ [lines, eol]
26
+ end
27
+
28
+ # The index after the block whose header is lines[+at+]: its indented
29
+ # lines, blank lines and comments.
30
+ def block_stop(lines, at)
31
+ stop = at + 1
32
+ stop += 1 while stop < lines.length && inside_block?(lines[stop])
33
+ stop
34
+ end
35
+
36
+ # Where a new child line of the block at +at+ (ending at +stop+) goes:
37
+ # its end, before trailing blank lines and comments shallower than a
38
+ # +child+ line.
39
+ def insert_at(lines, at, stop, child)
40
+ stop -= 1 while stop > at + 1 && (lines[stop - 1].strip.empty? || (comment?(lines[stop - 1]) && indent(lines[stop - 1]) < child))
41
+ stop
42
+ end
43
+
44
+ def inside_block?(line)
45
+ return true if line.strip.empty?
46
+ return false if line.start_with?("---", "...")
47
+
48
+ line.start_with?(" ", "\t") || comment?(line)
49
+ end
50
+
51
+ def blank_or_comment?(line) = line.strip.empty? || comment?(line)
52
+ def comment?(line) = line.lstrip.start_with?("#")
53
+ def indent(line) = line[/\A */].length
54
+
55
+ def parse(text) = YAML.safe_load(text, permitted_classes: [], aliases: false)
56
+
57
+ # Anchors and aliases: chi's reader refuses aliases, and an edit can't
58
+ # know what an anchor shares. Text that doesn't parse counts too.
59
+ def anchors?(text)
60
+ stack = [Psych.parse_stream(text)]
61
+ until stack.empty?
62
+ node = stack.pop
63
+ return true if node.is_a?(Psych::Nodes::Alias) || (node.respond_to?(:anchor) && node.anchor)
64
+
65
+ stack.concat(Array(node.children)) if node.respond_to?(:children)
66
+ end
67
+ false
68
+ rescue Psych::Exception
69
+ true
70
+ end
71
+ end
72
+ end
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.51.0
4
+ version: 0.52.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -288,6 +288,7 @@ files:
288
288
  - lib/samagotchi/hooks/loader.rb
289
289
  - lib/samagotchi/hooks/registry.rb
290
290
  - lib/samagotchi/hooks/stream_watch.rb
291
+ - lib/samagotchi/host_config.rb
291
292
  - lib/samagotchi/host_model.rb
292
293
  - lib/samagotchi/host_registry.rb
293
294
  - lib/samagotchi/idle_client.rb
@@ -300,6 +301,7 @@ files:
300
301
  - lib/samagotchi/installed_versions.rb
301
302
  - lib/samagotchi/iteration_limit.rb
302
303
  - lib/samagotchi/kernel_loop.rb
304
+ - lib/samagotchi/last_turn.rb
303
305
  - lib/samagotchi/launch_mode.rb
304
306
  - lib/samagotchi/live_versions.rb
305
307
  - lib/samagotchi/llm/api_key.rb
@@ -355,6 +357,7 @@ files:
355
357
  - lib/samagotchi/model_price.rb
356
358
  - lib/samagotchi/model_profile.rb
357
359
  - lib/samagotchi/model_ref.rb
360
+ - lib/samagotchi/model_settings.rb
358
361
  - lib/samagotchi/models_command.rb
359
362
  - lib/samagotchi/muted_memories.rb
360
363
  - lib/samagotchi/note_command.rb
@@ -382,6 +385,7 @@ files:
382
385
  - lib/samagotchi/prompt_note.rb
383
386
  - lib/samagotchi/prompt_warmup.rb
384
387
  - lib/samagotchi/question_desk.rb
388
+ - lib/samagotchi/queued_command.rb
385
389
  - lib/samagotchi/recap_setup.rb
386
390
  - lib/samagotchi/recap_store.rb
387
391
  - lib/samagotchi/relay.rb
@@ -405,6 +409,7 @@ files:
405
409
  - lib/samagotchi/session_metrics.rb
406
410
  - lib/samagotchi/session_observer.rb
407
411
  - lib/samagotchi/session_retention.rb
412
+ - lib/samagotchi/session_setup.rb
408
413
  - lib/samagotchi/sessions_command.rb
409
414
  - lib/samagotchi/steer.rb
410
415
  - lib/samagotchi/steer_cut.rb
@@ -434,6 +439,7 @@ files:
434
439
  - lib/samagotchi/terminal_ui/version_lines.rb
435
440
  - lib/samagotchi/text_diff.rb
436
441
  - lib/samagotchi/thinking.rb
442
+ - lib/samagotchi/thinking_command.rb
437
443
  - lib/samagotchi/thinking_tails.rb
438
444
  - lib/samagotchi/thinking_ticker.rb
439
445
  - lib/samagotchi/thought_stream_splitter.rb
@@ -519,6 +525,7 @@ files:
519
525
  - lib/samagotchi/web/public/images.js
520
526
  - lib/samagotchi/web/public/index.html
521
527
  - lib/samagotchi/web/public/init_row.js
528
+ - lib/samagotchi/web/public/kept_commands.js
522
529
  - lib/samagotchi/web/public/live_timing.js
523
530
  - lib/samagotchi/web/public/live_turn.js
524
531
  - lib/samagotchi/web/public/llm_context_chip.js
@@ -543,6 +550,7 @@ files:
543
550
  - lib/samagotchi/web/public/strip.js
544
551
  - lib/samagotchi/web/public/tally.js
545
552
  - lib/samagotchi/web/public/task_stop.js
553
+ - lib/samagotchi/web/public/thinking_chip.js
546
554
  - lib/samagotchi/web/public/thinking_ticker.js
547
555
  - lib/samagotchi/web/public/timing.js
548
556
  - lib/samagotchi/web/public/turn_events.js
@@ -550,6 +558,7 @@ files:
550
558
  - lib/samagotchi/web/public/turn_model.js
551
559
  - lib/samagotchi/web/public/turn_view.js
552
560
  - lib/samagotchi/web/public/update_notice.js
561
+ - lib/samagotchi/web/public/vendor/lit-html.js
553
562
  - lib/samagotchi/web/qr.rb
554
563
  - lib/samagotchi/web/server.rb
555
564
  - lib/samagotchi/web/session_hub.rb
@@ -560,6 +569,7 @@ files:
560
569
  - lib/samagotchi/worker_inbound.rb
561
570
  - lib/samagotchi/worker_sidecar.rb
562
571
  - lib/samagotchi/worker_wakes.rb
572
+ - lib/samagotchi/yaml_lines.rb
563
573
  homepage: https://github.com/dm1try/samagotchi
564
574
  licenses:
565
575
  - MIT