samagotchi 0.42.0 → 0.44.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -1
  3. data/docs/configuration.md +26 -0
  4. data/docs/context.md +46 -0
  5. data/docs/internals/context-telemetry.md +4 -1
  6. data/docs/internals/engine.md +1 -0
  7. data/docs/plugins.md +27 -5
  8. data/docs/sessions.md +1 -1
  9. data/lib/samagotchi/bridge/card_store.rb +20 -9
  10. data/lib/samagotchi/bridge/turn_notice.rb +3 -1
  11. data/lib/samagotchi/bundles/github-pr/manifest.yml +4 -4
  12. data/lib/samagotchi/bundles/github-pr/plugin.rb +177 -1
  13. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  14. data/lib/samagotchi/context_sources.rb +40 -0
  15. data/lib/samagotchi/context_status.rb +18 -5
  16. data/lib/samagotchi/kernel_loop.rb +24 -9
  17. data/lib/samagotchi/llm/chat_loop.rb +4 -3
  18. data/lib/samagotchi/llm_context_forget.rb +6 -2
  19. data/lib/samagotchi/llm_context_notice.rb +96 -0
  20. data/lib/samagotchi/plugin/attached_context.rb +75 -11
  21. data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
  22. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -0
  23. data/lib/samagotchi/terminal_ui/formatting.rb +7 -0
  24. data/lib/samagotchi/terminal_ui/one_shot_sink.rb +1 -0
  25. data/lib/samagotchi/tool_declarations.rb +2 -1
  26. data/lib/samagotchi/version.rb +1 -1
  27. data/lib/samagotchi/web/app.rb +47 -7
  28. data/lib/samagotchi/web/message_parts.rb +90 -11
  29. data/lib/samagotchi/web/public/app.js +80 -13
  30. data/lib/samagotchi/web/public/ctx.js +27 -2
  31. data/lib/samagotchi/web/public/data.js +6 -3
  32. data/lib/samagotchi/web/public/index.html +12 -0
  33. data/lib/samagotchi/web/public/llm_context_chip.js +151 -21
  34. data/lib/samagotchi/web/public/model_pick.js +3 -1
  35. data/lib/samagotchi/web/public/model_picker.js +2 -0
  36. data/lib/samagotchi/web/public/session_state.js +2 -0
  37. data/lib/samagotchi/web/public/turn_events.js +34 -0
  38. data/lib/samagotchi/web/public/turn_html.js +33 -2
  39. data/lib/samagotchi/web/public/turn_view.js +4 -2
  40. metadata +2 -1
@@ -156,6 +156,39 @@ export function steerCutLine({ source } = {}) {
156
156
  return `↪ cut in for ${STEER_CUT_FOR[source] || "your message"}`;
157
157
  }
158
158
 
159
+ // The ✂ row of a batch of LLM context edits the server applied
160
+ // (llm_context_edited, LLMContextNotice): its line as the server built
161
+ // it ("✂ forgot 2 outputs, stubbed 1 stale read · frees ~4.1k tokens"),
162
+ // so the web and the TUI say the same.
163
+ export function llmContextLine({ text } = {}) {
164
+ return text || "✂ LLM context edited";
165
+ }
166
+
167
+ // The ✂ row's hover: what each group did, one line per output (its id,
168
+ // tool and title), and what the batch costs ("~": chars/4).
169
+ export function llmContextHover({ groups = [], freed_tokens: freed, tail_tokens: tail, staged, moment } = {}) {
170
+ const lines = [];
171
+ const output = (item) => [item.id, item.tool, item.title].filter(Boolean).join(" ");
172
+ for (const group of Array.isArray(groups) ? groups : []) {
173
+ const items = Array.isArray(group.items) ? group.items : [];
174
+ if (group.kind === "stale") {
175
+ lines.push("Stubbed as stale:");
176
+ for (const item of items) lines.push(` ${output(item)}${item.note ? ` — ${item.note}` : ""}`);
177
+ } else if (group.kind === "forget") {
178
+ lines.push(`Forgotten by the ${group.by || "model"}: ${group.note || ""}`);
179
+ for (const item of items) lines.push(` ${output(item)}${item.kept ? ` · lines ${item.kept} kept` : ""}`);
180
+ }
181
+ }
182
+ const cost = [];
183
+ if (Number(freed) > 0) cost.push(`frees ~${freed} tokens`);
184
+ if (Number(tail) > 0) cost.push(`the server reads ~${tail} tokens again`);
185
+ if (cost.length) lines.push(cost.join("; "));
186
+ if (Number(staged) > 0) lines.push(`${staged} more staged until the turn ends`);
187
+ lines.push(moment === "turn_end" ? "Sent from the next turn on." : "Sent from the next request on.");
188
+ lines.push("The session keeps the originals.");
189
+ return lines.join("\n");
190
+ }
191
+
159
192
  // The one muted line for a turn that ended with no answer (turn_completed's
160
193
  // turn_summary.empty_answer, or a reloaded turn's marker): the TUI words
161
194
  // it the same (TurnNote.empty_answer_line; spec/shared/labels_matrix.json).
@@ -170,6 +203,7 @@ export function emptyAnswerLine({ retries = 0 } = {}) {
170
203
  export function noticeLine(entry = {}) {
171
204
  if (entry.type === "empty_answer_retry") return emptyRetryLine(entry);
172
205
  if (entry.type === "steer_cut") return steerCutLine(entry);
206
+ if (entry.type === "llm_context_edited") return llmContextLine(entry);
173
207
  return `${hookNoticeLabel(entry.hook)}: ${entry.text || ""}`;
174
208
  }
175
209
 
@@ -33,12 +33,43 @@ function statusWordHtml({ cls, word }) {
33
33
  return word ? `<span class="activity-state ${cls}">${word}</span>` : "";
34
34
  }
35
35
 
36
+ // A reloaded row's ✂ mark: what an LLM context edit saved on its output
37
+ // sends the model instead (MessageParts.edit_marks): a stale stub and
38
+ // what it frees, a forget's note (or the output whose row carries it) and
39
+ // its kept lines, a forget still staged. The live rows get none (a reload
40
+ // shows them).
41
+ export function llmEditLine(edit) {
42
+ if (!edit || typeof edit !== "object") return "";
43
+ const kept = edit.kept ? ` · lines ${edit.kept} kept` : "";
44
+ if (edit.kind === "forget") {
45
+ if (edit.staged) return `✂ forget staged${kept}`;
46
+ return `${edit.with ? `✂ forgotten with ${edit.with}` : `✂ forgotten: ${edit.note || ""}`}${kept}`;
47
+ }
48
+ if (edit.kind === "stale") {
49
+ const tokens = Number(edit.tokens) > 0 ? ` · ~${compactTokens(Number(edit.tokens))}` : "";
50
+ return `✂ stubbed: ${edit.note || "stale"}${tokens}`;
51
+ }
52
+ return "";
53
+ }
54
+
55
+ function compactTokens(n) {
56
+ return n >= 1000 ? `${(n / 1000).toFixed(1)}k tokens` : `${n} tokens`;
57
+ }
58
+
59
+ function llmEditHtml(row) {
60
+ const line = llmEditLine(row.edit);
61
+ if (!line) return "";
62
+ const hover = `${row.tool_id ? `${row.tool_id}: ` : ""}${line}\nThe model is sent this instead of the output; the session keeps it.`;
63
+ return `<div class="activity-edit" title="${escapeHtml(hover)}">${escapeHtml(line)}</div>`;
64
+ }
65
+
36
66
  // A tool row: its status mark, tool, title (else params; the full command,
37
67
  // else the full params line, on hover), the word an exception shows, the
38
68
  // call's duration, the full command (an execute/task_create's view, also
39
69
  // while it runs), the output once done (its "[tool]" tag dropped, 300 chars,
40
70
  // the rest on hover), its images (+thumbs+(images) draws them) and an
41
- // edit's collapsed diff. A part with nothing to say is left out.
71
+ // edit's collapsed diff, and a reloaded row's ✂ mark above its output. A
72
+ // part with nothing to say is left out.
42
73
  export function toolRowHtml(row, { thumbs = () => "" } = {}) {
43
74
  return `<div class="activity-row" data-key="${escapeHtml(row.key)}">${toolRowInnerHtml(row, { thumbs })}</div>`;
44
75
  }
@@ -55,7 +86,7 @@ export function toolRowInnerHtml(row, { thumbs = () => "" } = {}) {
55
86
  const status = statusParts(row.status);
56
87
  return `${statusMarkHtml(status)}<span class="activity-tool">${escapeHtml(toolName(row))}</span>${params}${statusWordHtml(status)}` +
57
88
  `${duration ? `<span class="activity-duration">${escapeHtml(duration)}</span>` : ""}` +
58
- `${commandBlockHtml(row.view, row.title)}${output}` +
89
+ `${commandBlockHtml(row.view, row.title)}${llmEditHtml(row)}${output}` +
59
90
  `${row.images?.length ? thumbs(row.images) : ""}${row.diff ? activityDiffHtml(row.diff) : ""}`;
60
91
  }
61
92
 
@@ -422,15 +422,17 @@ export function createTurnView({
422
422
  // A hook's line (hook_notice) as a row of the current step, above the row
423
423
  // of the call it judged; false with no running turn (app.js shows the
424
424
  // bubble then). data.line is a whole row of chi's own (an empty-answer
425
- // retry), shown as is.
425
+ // retry), shown as is; data.cls a class of its own and data.title its
426
+ // hover (a ✂ row's).
426
427
  function notice(data) {
427
428
  const r = turn && model.notice(turn, data);
428
429
  if (!r) return false;
429
430
  const entry = genEls.get(r.gen);
430
431
  if (!r.gen.text) entry.text.textContent = "";
431
432
  const el = document.createElement("div");
432
- el.className = "hook-notice" + (data.level === "warn" ? " warn" : "");
433
+ el.className = "hook-notice" + (data.level === "warn" ? " warn" : "") + (data.cls ? ` ${data.cls}` : "");
433
434
  el.textContent = data.line || `${hookNoticeLabel(data.hook)}: ${data.text || ""}`;
435
+ if (data.title) el.title = data.title;
434
436
  const stick = isNearBottom();
435
437
  activityEl(r.gen).appendChild(el);
436
438
  if (stick) follow();
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.42.0
4
+ version: 0.44.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -315,6 +315,7 @@ files:
315
315
  - lib/samagotchi/llm_context_command.rb
316
316
  - lib/samagotchi/llm_context_edit.rb
317
317
  - lib/samagotchi/llm_context_forget.rb
318
+ - lib/samagotchi/llm_context_notice.rb
318
319
  - lib/samagotchi/llm_context_override.rb
319
320
  - lib/samagotchi/llm_context_stale.rb
320
321
  - lib/samagotchi/llm_context_strategy.rb