samagotchi 0.2.0 → 0.4.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 (131) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +198 -1
  3. data/README.md +56 -4
  4. data/bin/chi +118 -50
  5. data/docs/cli.md +184 -9
  6. data/docs/configuration.md +333 -47
  7. data/docs/desktop.md +45 -4
  8. data/docs/guardrails.md +11 -0
  9. data/docs/hooks.md +208 -5
  10. data/docs/plugins.md +68 -2
  11. data/docs/releasing.md +23 -13
  12. data/docs/sessions.md +45 -17
  13. data/lib/samagotchi/answer_display.rb +95 -0
  14. data/lib/samagotchi/archive_store.rb +90 -0
  15. data/lib/samagotchi/bootstrap/config_writer.rb +342 -0
  16. data/lib/samagotchi/bootstrap/probe.rb +262 -0
  17. data/lib/samagotchi/bootstrap_command.rb +347 -0
  18. data/lib/samagotchi/bridge/pending_card.rb +89 -0
  19. data/lib/samagotchi/bridge/turn_accumulator.rb +15 -3
  20. data/lib/samagotchi/bridge.rb +13 -1
  21. data/lib/samagotchi/bridge_client.rb +6 -2
  22. data/lib/samagotchi/bundles/check-in/manifest.yml +10 -0
  23. data/lib/samagotchi/bundles/check-in/plugin.rb +244 -0
  24. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +531 -0
  25. data/lib/samagotchi/bundles/source-links/manifest.yml +14 -0
  26. data/lib/samagotchi/bundles/source-links/source_links.md +5 -0
  27. data/lib/samagotchi/bundles/system/config_modification_protocol.md +10 -6
  28. data/lib/samagotchi/bundles/system/delegated.md +6 -7
  29. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  30. data/lib/samagotchi/bundles/system/self_map.md +8 -2
  31. data/lib/samagotchi/client.rb +81 -19
  32. data/lib/samagotchi/commands/registry.rb +8 -0
  33. data/lib/samagotchi/config.rb +252 -48
  34. data/lib/samagotchi/desktop/macos/App.swift +12 -8
  35. data/lib/samagotchi/desktop/macos/ChiRunner.swift +17 -9
  36. data/lib/samagotchi/desktop/macos/Images.swift +113 -0
  37. data/lib/samagotchi/desktop/macos/Info.plist.erb +6 -0
  38. data/lib/samagotchi/desktop/macos/Panel.swift +180 -25
  39. data/lib/samagotchi/desktop/macos.rb +59 -8
  40. data/lib/samagotchi/desktop_command.rb +6 -3
  41. data/lib/samagotchi/edit_preview.rb +82 -0
  42. data/lib/samagotchi/empty_answer_retry.rb +43 -0
  43. data/lib/samagotchi/engine.rb +434 -140
  44. data/lib/samagotchi/gem_update.rb +89 -0
  45. data/lib/samagotchi/guardrails/approval.rb +35 -4
  46. data/lib/samagotchi/guardrails/load_failures.rb +9 -3
  47. data/lib/samagotchi/guardrails/scratch_writes.rb +40 -0
  48. data/lib/samagotchi/guardrails.rb +1 -0
  49. data/lib/samagotchi/hooks/registry.rb +24 -5
  50. data/lib/samagotchi/host_registry.rb +9 -12
  51. data/lib/samagotchi/idle_client.rb +24 -15
  52. data/lib/samagotchi/idle_recap.rb +5 -1
  53. data/lib/samagotchi/idle_reminders.rb +2 -2
  54. data/lib/samagotchi/image_store.rb +10 -6
  55. data/lib/samagotchi/kernel_loop.rb +73 -94
  56. data/lib/samagotchi/live_versions.rb +59 -0
  57. data/lib/samagotchi/llm/api_key.rb +41 -0
  58. data/lib/samagotchi/llm/chat_loop.rb +132 -29
  59. data/lib/samagotchi/llm/errors.rb +41 -9
  60. data/lib/samagotchi/llm/http.rb +57 -17
  61. data/lib/samagotchi/llm/openai_chat.rb +17 -30
  62. data/lib/samagotchi/log_subscriber.rb +18 -3
  63. data/lib/samagotchi/memory_bundle/installer.rb +65 -63
  64. data/lib/samagotchi/memory_bundle/provenance.rb +51 -12
  65. data/lib/samagotchi/memory_bundle/shipped_update.rb +157 -0
  66. data/lib/samagotchi/memory_bundle/status.rb +4 -1
  67. data/lib/samagotchi/memory_bundle/system_bundle.rb +81 -53
  68. data/lib/samagotchi/model_profile.rb +24 -1
  69. data/lib/samagotchi/plugin/context.rb +22 -1
  70. data/lib/samagotchi/plugin/sessions.rb +3 -1
  71. data/lib/samagotchi/prompt.rb +4 -2
  72. data/lib/samagotchi/reminder_store.rb +1 -9
  73. data/lib/samagotchi/reply_wait.rb +126 -0
  74. data/lib/samagotchi/sampling_settings.rb +58 -0
  75. data/lib/samagotchi/self_report.rb +18 -3
  76. data/lib/samagotchi/send_command.rb +252 -11
  77. data/lib/samagotchi/session.rb +52 -11
  78. data/lib/samagotchi/session_archive_command.rb +107 -0
  79. data/lib/samagotchi/session_commands.rb +46 -7
  80. data/lib/samagotchi/session_manager.rb +115 -25
  81. data/lib/samagotchi/session_metrics.rb +222 -106
  82. data/lib/samagotchi/steer.rb +72 -0
  83. data/lib/samagotchi/terminal_ui/attached_loop.rb +57 -28
  84. data/lib/samagotchi/terminal_ui/event_renderer.rb +21 -11
  85. data/lib/samagotchi/terminal_ui/formatting.rb +40 -8
  86. data/lib/samagotchi/terminal_ui/input_support.rb +7 -19
  87. data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
  88. data/lib/samagotchi/terminal_ui.rb +134 -247
  89. data/lib/samagotchi/text_diff.rb +181 -0
  90. data/lib/samagotchi/thinking.rb +115 -0
  91. data/lib/samagotchi/tool_activity.rb +3 -1
  92. data/lib/samagotchi/tool_runner.rb +34 -1
  93. data/lib/samagotchi/tools/ask_user_question.rb +41 -33
  94. data/lib/samagotchi/tools/builtins.rb +15 -4
  95. data/lib/samagotchi/tools/delegate_wait.rb +26 -69
  96. data/lib/samagotchi/tools/edit.rb +23 -9
  97. data/lib/samagotchi/tools/execute.rb +52 -14
  98. data/lib/samagotchi/tools/task_runtime.rb +19 -0
  99. data/lib/samagotchi/tools/task_wait.rb +27 -3
  100. data/lib/samagotchi/tools/write.rb +4 -0
  101. data/lib/samagotchi/turn_flow.rb +12 -2
  102. data/lib/samagotchi/turn_note.rb +60 -6
  103. data/lib/samagotchi/update_command.rb +308 -0
  104. data/lib/samagotchi/update_hint.rb +59 -0
  105. data/lib/samagotchi/version.rb +1 -1
  106. data/lib/samagotchi/vision_support.rb +7 -9
  107. data/lib/samagotchi/web/app.rb +91 -7
  108. data/lib/samagotchi/web/message_parts.rb +8 -3
  109. data/lib/samagotchi/web/public/activity.js +13 -1
  110. data/lib/samagotchi/web/public/annotate_presets.js +26 -0
  111. data/lib/samagotchi/web/public/annotations.js +13 -0
  112. data/lib/samagotchi/web/public/app.js +472 -111
  113. data/lib/samagotchi/web/public/card.js +5 -3
  114. data/lib/samagotchi/web/public/chat_view.js +13 -1
  115. data/lib/samagotchi/web/public/copy.js +20 -4
  116. data/lib/samagotchi/web/public/ctx.js +15 -0
  117. data/lib/samagotchi/web/public/data.js +23 -6
  118. data/lib/samagotchi/web/public/diff_view.js +58 -0
  119. data/lib/samagotchi/web/public/format.js +9 -0
  120. data/lib/samagotchi/web/public/index.html +60 -3
  121. data/lib/samagotchi/web/public/notify.js +175 -0
  122. data/lib/samagotchi/web/public/question_card.js +5 -2
  123. data/lib/samagotchi/web/public/sessions_list.js +7 -0
  124. data/lib/samagotchi/web/public/timing.js +39 -14
  125. data/lib/samagotchi/web/public/turn_events.js +75 -5
  126. data/lib/samagotchi/web/public/turn_view.js +49 -8
  127. data/lib/samagotchi/web/server.rb +8 -4
  128. data/lib/samagotchi/web/session_hub.rb +2 -1
  129. data/lib/samagotchi/web/session_summary.rb +24 -1
  130. data/lib/samagotchi/worker.rb +16 -4
  131. metadata +31 -1
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "text_diff"
4
+ require_relative "tools/edit"
5
+ require_relative "tools/tool_path"
6
+
7
+ module Samagotchi
8
+ # What chi's own edit/write tools change, as a TextDiff. Used twice:
9
+ #
10
+ # - EditPreview.for(call): a dry run before the call runs (the approval
11
+ # card). It reads the file once and never writes.
12
+ # - EditPreview.snapshot + change: the file before and after the call ran
13
+ # (the tool row), so the row shows what really happened.
14
+ #
15
+ # A diff is {text:, added:, removed:, truncated:, new_file:}; a preview can
16
+ # also be {error: "old text not found in …"} (the call would fail) or
17
+ # {skipped: "binary file" | "file over 1 MB"}.
18
+ module EditPreview
19
+ TOOLS = %w[edit write].freeze
20
+ MAX_FILE_BYTES = 1024 * 1024
21
+ SNIFF_BYTES = 8 * 1024
22
+
23
+ module_function
24
+
25
+ def tool?(name) = TOOLS.include?(name.to_s)
26
+
27
+ # @return [Hash, nil] nil for a tool that isn't edit/write
28
+ def for(call)
29
+ return nil unless tool?(call[:name])
30
+
31
+ path = Tools::ToolPath.normalize(call[:path])
32
+ before = snapshot(path)
33
+ return before if before.is_a?(Hash)
34
+
35
+ if call[:name].to_s == "edit"
36
+ result = Tools::Edit.apply(call[:content], path: path, start_line: call[:start_line],
37
+ end_line: call[:end_line], read: ->(_) { before })
38
+ return { error: result.delete_prefix("Error: ") } if result.is_a?(String)
39
+
40
+ after = result.first
41
+ else
42
+ after = call[:content]
43
+ return { error: "missing content" } unless after.is_a?(String)
44
+
45
+ skip = skip_reason(after)
46
+ return { skipped: skip } if skip
47
+ end
48
+ # Unlike change, a no-op still gets a (0/0) diff: nil means "not edit/write".
49
+ TextDiff.unified(before.to_s, after).merge(new_file: before.nil?)
50
+ end
51
+
52
+ # The file as the tools read it: nil when it doesn't exist, the text, or
53
+ # {skipped:} / {error:} when it can't be diffed.
54
+ def snapshot(path)
55
+ return nil unless File.file?(path)
56
+ return { skipped: "file over 1 MB" } if File.size(path) > MAX_FILE_BYTES
57
+
58
+ text = File.read(path)
59
+ skip = skip_reason(text)
60
+ skip ? { skipped: skip } : text
61
+ rescue SystemCallError, IOError => e
62
+ { error: e.message }
63
+ end
64
+
65
+ # @param before [String, nil] nil for a file that didn't exist
66
+ # @return [Hash, nil] nil when there's nothing to show (same text, or
67
+ # the after side isn't a readable text file)
68
+ def change(before, after)
69
+ return nil unless after.is_a?(String)
70
+ return nil unless before.nil? || before.is_a?(String)
71
+ return nil if before == after
72
+
73
+ TextDiff.unified(before.to_s, after).merge(new_file: before.nil?)
74
+ end
75
+
76
+ def skip_reason(text)
77
+ return "file over 1 MB" if text.bytesize > MAX_FILE_BYTES
78
+
79
+ "binary file" if text.byteslice(0, SNIFF_BYTES).include?("\0")
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config"
4
+ require_relative "turn_note"
5
+
6
+ module Samagotchi
7
+ # Asking the model again, in the same turn, when a generation ends with no
8
+ # visible text and no tool calls (thinking only, or nothing): a hidden
9
+ # nudge (TurnNote.empty_retry), then another generation. Both loops use
10
+ # it; the Engine's TurnNote.empty is what's left when the retries run out.
11
+ module EmptyAnswerRetry
12
+ MAX = 3
13
+ # Greedy decoding on a near-identical prompt would likely loop again:
14
+ # the retry runs at this temperature unless one is configured.
15
+ TEMPERATURE = 0.6
16
+ # A generation cut at the provider's output cap (finish_reason length)
17
+ # is retried unless the context is this full.
18
+ CONTEXT_FULL_RATIO = 0.9
19
+
20
+ module_function
21
+
22
+ # retry.empty_answer, 0..MAX.
23
+ def limit
24
+ Config.get("retry.empty_answer").to_i.clamp(0, MAX)
25
+ rescue StandardError
26
+ 1
27
+ end
28
+
29
+ # The request options of a retry generation: the turn's sampling, with
30
+ # TEMPERATURE when it sets none (a configured nil keeps "don't send").
31
+ def sampling(base)
32
+ base = base || {}
33
+ base.key?(:temperature) ? base : base.merge(temperature: TEMPERATURE)
34
+ end
35
+
36
+ # The prompt filled the window (a length stop is then no loop to break).
37
+ def context_full?(used_tokens, window_tokens)
38
+ return false unless used_tokens.to_i.positive? && window_tokens.to_i.positive?
39
+
40
+ used_tokens.to_f / window_tokens >= CONTEXT_FULL_RATIO
41
+ end
42
+ end
43
+ end