samagotchi 0.54.0 → 0.56.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -1
  3. data/bin/chi +3 -2
  4. data/docs/architecture.md +6 -2
  5. data/docs/cli.md +29 -7
  6. data/docs/configuration.md +113 -8
  7. data/docs/internals/background-tasks.md +1 -1
  8. data/docs/internals/context-telemetry.md +23 -5
  9. data/docs/internals/engine.md +54 -18
  10. data/docs/internals/prompt-caching.md +8 -0
  11. data/docs/sessions.md +2 -2
  12. data/docs/testing.md +23 -3
  13. data/lib/samagotchi/bridge/turn_accumulator.rb +6 -6
  14. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  15. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  16. data/lib/samagotchi/cache_confirm.rb +51 -0
  17. data/lib/samagotchi/client_id.rb +12 -0
  18. data/lib/samagotchi/config.rb +9 -3
  19. data/lib/samagotchi/engine.rb +102 -8
  20. data/lib/samagotchi/engine_peers.rb +1 -0
  21. data/lib/samagotchi/file_ref.rb +59 -0
  22. data/lib/samagotchi/host_config.rb +6 -3
  23. data/lib/samagotchi/host_registry.rb +4 -3
  24. data/lib/samagotchi/kernel_loop.rb +99 -16
  25. data/lib/samagotchi/llm/chat_loop.rb +122 -71
  26. data/lib/samagotchi/llm/openai_chat.rb +2 -1
  27. data/lib/samagotchi/llm/stream_drops.rb +63 -0
  28. data/lib/samagotchi/llm/turn_settings.rb +9 -5
  29. data/lib/samagotchi/model_settings.rb +5 -2
  30. data/lib/samagotchi/reasoning_history.rb +132 -0
  31. data/lib/samagotchi/reasoning_history_command.rb +107 -0
  32. data/lib/samagotchi/self_report.rb +21 -0
  33. data/lib/samagotchi/session.rb +23 -2
  34. data/lib/samagotchi/session_commands.rb +55 -15
  35. data/lib/samagotchi/session_manager.rb +2 -1
  36. data/lib/samagotchi/session_setup.rb +7 -5
  37. data/lib/samagotchi/setting_explained.rb +20 -0
  38. data/lib/samagotchi/terminal_ui/attach_launcher.rb +2 -1
  39. data/lib/samagotchi/terminal_ui/attached_loop.rb +11 -0
  40. data/lib/samagotchi/terminal_ui/formatting.rb +8 -5
  41. data/lib/samagotchi/terminal_ui.rb +13 -6
  42. data/lib/samagotchi/thinking.rb +20 -25
  43. data/lib/samagotchi/thinking_command.rb +33 -11
  44. data/lib/samagotchi/tool_row_fields.rb +46 -0
  45. data/lib/samagotchi/tool_runner.rb +7 -11
  46. data/lib/samagotchi/tools/builtins.rb +6 -2
  47. data/lib/samagotchi/tools/peers.rb +4 -2
  48. data/lib/samagotchi/tools/task_runtime.rb +2 -1
  49. data/lib/samagotchi/version.rb +1 -1
  50. data/lib/samagotchi/web/app.rb +48 -4
  51. data/lib/samagotchi/web/capabilities.rb +13 -9
  52. data/lib/samagotchi/web/editor.rb +38 -0
  53. data/lib/samagotchi/web/message_parts.rb +5 -10
  54. data/lib/samagotchi/web/public/activity.js +33 -18
  55. data/lib/samagotchi/web/public/app.js +256 -1133
  56. data/lib/samagotchi/web/public/commands.js +269 -0
  57. data/lib/samagotchi/web/public/composer.js +105 -0
  58. data/lib/samagotchi/web/public/diff_view.js +30 -4
  59. data/lib/samagotchi/web/public/index.html +11 -3
  60. data/lib/samagotchi/web/public/info_bar.js +80 -0
  61. data/lib/samagotchi/web/public/level_chip.js +174 -0
  62. data/lib/samagotchi/web/public/lists_view.js +725 -0
  63. data/lib/samagotchi/web/public/llm_context_chip.js +9 -6
  64. data/lib/samagotchi/web/public/model_pick.js +1 -1
  65. data/lib/samagotchi/web/public/question_cards.js +2 -1
  66. data/lib/samagotchi/web/public/reasoning_history_chip.js +49 -0
  67. data/lib/samagotchi/web/public/refs.js +134 -0
  68. data/lib/samagotchi/web/public/restore.js +81 -0
  69. data/lib/samagotchi/web/public/session_state.js +57 -0
  70. data/lib/samagotchi/web/public/stream_handlers.js +332 -0
  71. data/lib/samagotchi/web/public/thinking_chip.js +38 -144
  72. data/lib/samagotchi/web/public/turn_events.js +17 -258
  73. data/lib/samagotchi/web/public/turn_html.js +5 -1
  74. data/lib/samagotchi/web/server.rb +4 -4
  75. data/lib/samagotchi/worker.rb +17 -5
  76. metadata +18 -2
  77. data/lib/samagotchi/web/public/stable_html.js +0 -18
@@ -0,0 +1,174 @@
1
+ // A level chip in the session bar and its popover: the level the next turn
2
+ // runs at ("think low", "reasoning all"; "· session" when the session set
3
+ // its own) and a form with a level select that runs the setting's command
4
+ // in the session's worker (the reply shows in the conversation, like a
5
+ // typed one). The summary is the setting's SettingExplained#summary
6
+ // ({level, source, own}: the worker's snapshot, a command_ran, or the
7
+ // server's own reading of the session file).
8
+ //
9
+ // A config names the setting (thinking_chip.js, reasoning_history_chip.js):
10
+ // name the popover's heading ("Thinking")
11
+ // prefix the chip's word ("think")
12
+ // command the command ("/thinking")
13
+ // levels the settable levels (the select's options after "follow")
14
+ // anchor the chip's selector (its data attribute: each config's
15
+ // chipTemplate draws it; clicks are delegated on the same
16
+ // element as the other chips')
17
+ // formClass, popoverClass, nowClass the form's, popover's and "Now:"
18
+ // line's own classes
19
+ // when(fresh) the form's note on when it applies (fresh: the start
20
+ // page's new chat, thinking only)
21
+ //
22
+ // The start page's new chat (kind NEW_CHAT) has a thinking chip only
23
+ // (thinking_chip.js keeps its choice): the form shows the model's level and
24
+ // takes a choice for the next create.
25
+
26
+ import { NEW_CHAT, SESSION, popoverPlace } from "./llm_context_chip.js";
27
+ import { html, nothing, render } from "./vendor/lit-html.js";
28
+
29
+ export const DEFAULT = "default";
30
+
31
+ // The chip: its text, tooltip and whether the session (or the new chat)
32
+ // set its own; null without a summary (an older worker, or a host the
33
+ // setting means nothing on).
34
+ export function chipModel(cfg, summary, { kind = SESSION } = {}) {
35
+ if (!summary || !summary.level) return null;
36
+ const own = !!summary.own;
37
+ const what = kind === NEW_CHAT ? "the new chat" : "this session";
38
+ const from = own ? what : summary.source || "the model's own";
39
+ const title = `${cfg.name}: ${summary.level} (${from}). Click to change it for ${what}.`;
40
+ return { text: `${cfg.prefix} ${summary.level}${own ? ` · ${kind}` : ""}`, title, own };
41
+ }
42
+
43
+ // The command line for a select value ("default" unsets the session's own),
44
+ // with --yes: picking a level in the form is the choice, and the form
45
+ // names the cost (CacheConfirm asks nothing then); null when it is what
46
+ // the form opened with.
47
+ export function commandLine(cfg, level, initial = null) {
48
+ if (initial !== null && level === initial) return null;
49
+ return `${cfg.command} ${level} --yes`;
50
+ }
51
+
52
+ // "Now: low (session)", "Now: default (the model's own)".
53
+ export function nowText(summary) {
54
+ return `Now: ${summary.level} (${summary.source || "the model's own"})`;
55
+ }
56
+
57
+ const option = (value, label, chosen) => html`<option value=${value} ?selected=${value === chosen}>${label}</option>`;
58
+
59
+ // The popover's form (a lit-html template). For the new chat (kind
60
+ // NEW_CHAT), +summary+ is the model's and +choice+ the level the form
61
+ // shows; +now+ is the summary its "Now:" line reads (refresh passes a
62
+ // newer one, the rest stays as the form opened).
63
+ export function formTemplate(cfg, summary, { running = false, kind = SESSION, choice = null, now = summary } = {}) {
64
+ const fresh = kind === NEW_CHAT;
65
+ const chosen = (fresh ? choice : summary.own) || DEFAULT;
66
+ const follow = summary.own && !fresh ? "follow the model" : `follow the model (${summary.level})`;
67
+ return html`<form class=${`${cfg.formClass} llmctx-form`}><div class="ctx-pop-head"><strong>${cfg.name}</strong
68
+ ><span class="ctx-pop-kind">${fresh ? NEW_CHAT : "this session"}</span
69
+ ><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
70
+ ><div class=${`ctx-pop-meta ${cfg.nowClass}`}>${nowText(now)}</div
71
+ ><label class="llmctx-field"><span>Level</span><select name="level">${option(DEFAULT, follow, chosen)}${
72
+ cfg.levels.map((level) => option(level, level, chosen))}</select></label
73
+ ><div class="ctx-pop-meta">${cfg.when(fresh)}${running ? " A turn is running: it applies after this turn." : ""}</div
74
+ ><div class="ctx-pop-actions"><button type="submit" class="llmctx-set">Set</button></div></form>`;
75
+ }
76
+
77
+ // The chip's popover, as createLlmContextControl's: el is where the chip
78
+ // is drawn (clicks are delegated: #infoText, info_bar.js);
79
+ // summary(): the open session's summary; session(): its id; running(): a
80
+ // turn runs; run(line): sends the command. The start page's (kind
81
+ // NEW_CHAT): summary() is the picked model's, choice() the pending level
82
+ // and choose(next) takes a Set's (null for "follow the model"). id and
83
+ // anchor (the chip's selector, by default its data attribute) keep it
84
+ // apart from the other controls on the same element (the anchor defaults
85
+ // to the config's).
86
+ export function createLevelControl({ cfg, el, summary, session = () => null, running = () => false, run,
87
+ kind = SESSION, choice = () => null, choose = null,
88
+ id, anchor: anchorSel = cfg.anchor,
89
+ doc = document, win = window }) {
90
+ const fresh = kind === NEW_CHAT;
91
+ const pop = doc.createElement("div");
92
+ pop.id = id;
93
+ pop.className = `ctx-popover ${cfg.popoverClass}`;
94
+ pop.setAttribute("role", "dialog");
95
+ pop.hidden = true;
96
+ doc.body.appendChild(pop);
97
+ let initial = null;
98
+ let openFor = null;
99
+ // What the open form was drawn from: the model's or session's summary
100
+ // and the form's options. A closed popover is empty, so each open draws
101
+ // a fresh form (an unsent pick is gone).
102
+ let shown = null;
103
+ const draw = (now) => render(formTemplate(cfg, shown.data, { ...shown.opts, now }), pop);
104
+
105
+ function place(anchor) {
106
+ const width = Math.min(320, win.innerWidth - 32);
107
+ pop.style.width = `${width}px`;
108
+ pop.style.maxHeight = "";
109
+ const at = popoverPlace(anchor.getBoundingClientRect(), { width, height: pop.offsetHeight,
110
+ innerWidth: win.innerWidth, innerHeight: win.innerHeight });
111
+ pop.style.left = `${at.left}px`;
112
+ pop.style.top = at.top === undefined ? "" : `${at.top}px`;
113
+ pop.style.bottom = at.bottom === undefined ? "" : `${at.bottom}px`;
114
+ if (at.maxHeight !== undefined) pop.style.maxHeight = `${at.maxHeight}px`;
115
+ }
116
+
117
+ function open(anchor) {
118
+ const data = summary();
119
+ if (!data) return;
120
+ initial = (fresh ? choice() : data.own) || DEFAULT;
121
+ openFor = session();
122
+ shown = { data, opts: { running: running(), kind, choice: choice() } };
123
+ draw(data);
124
+ pop.hidden = false;
125
+ place(anchor);
126
+ }
127
+
128
+ function close() {
129
+ pop.hidden = true;
130
+ initial = null;
131
+ openFor = null;
132
+ shown = null;
133
+ render(nothing, pop);
134
+ }
135
+
136
+ function isOpen() { return !pop.hidden; }
137
+
138
+ // A new summary: an open form keeps the user's pick; only "Now:" follows
139
+ // (the same template again: lit writes that line alone).
140
+ function refresh() {
141
+ if (!isOpen()) return;
142
+ const data = summary();
143
+ if (!data || session() !== openFor) return close();
144
+ draw(data);
145
+ }
146
+
147
+ el.addEventListener("click", (e) => {
148
+ const anchor = e.target.closest(anchorSel);
149
+ if (!anchor) return;
150
+ e.stopPropagation();
151
+ if (isOpen()) close();
152
+ else open(anchor);
153
+ });
154
+ pop.addEventListener("submit", (e) => {
155
+ e.preventDefault();
156
+ const level = e.target.elements.level.value;
157
+ const was = initial;
158
+ close();
159
+ if (fresh) return choose(level === DEFAULT ? null : level);
160
+ const line = commandLine(cfg, level, was);
161
+ if (line) run(line);
162
+ });
163
+ pop.addEventListener("click", (e) => {
164
+ if (e.target.closest("[data-act]")?.dataset.act === "close") close();
165
+ });
166
+ doc.addEventListener("click", (e) => {
167
+ if (isOpen() && !pop.contains(e.target) && !e.target.closest?.(anchorSel)) close();
168
+ });
169
+ doc.addEventListener("keydown", (e) => {
170
+ if (e.key === "Escape" && isOpen()) close();
171
+ });
172
+
173
+ return { close, refresh, get open() { return isOpen(); } };
174
+ }