samagotchi 0.14.0 → 0.16.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -1
  3. data/docs/architecture.md +10 -0
  4. data/docs/cli.md +3 -2
  5. data/docs/configuration.md +3 -1
  6. data/docs/guardrails.md +16 -2
  7. data/docs/plugins.md +3 -3
  8. data/docs/sessions.md +7 -2
  9. data/docs/sub-agent.md +15 -3
  10. data/lib/samagotchi/answer_display.rb +6 -2
  11. data/lib/samagotchi/bridge/card_store.rb +16 -1
  12. data/lib/samagotchi/bridge/turn_accumulator.rb +6 -0
  13. data/lib/samagotchi/bridge.rb +84 -0
  14. data/lib/samagotchi/bridge_client.rb +33 -6
  15. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  16. data/lib/samagotchi/bundles/system/self_map.md +2 -1
  17. data/lib/samagotchi/empty_answer_retry.rb +2 -1
  18. data/lib/samagotchi/engine.rb +58 -17
  19. data/lib/samagotchi/kernel_loop.rb +16 -5
  20. data/lib/samagotchi/llm/chat_loop.rb +17 -9
  21. data/lib/samagotchi/llm/model_result.rb +12 -7
  22. data/lib/samagotchi/log_subscriber.rb +1 -0
  23. data/lib/samagotchi/parent_report.rb +8 -0
  24. data/lib/samagotchi/plugin_session_state.rb +30 -0
  25. data/lib/samagotchi/question_desk.rb +97 -18
  26. data/lib/samagotchi/relay.rb +88 -0
  27. data/lib/samagotchi/relay_desk.rb +73 -0
  28. data/lib/samagotchi/relay_verifier.rb +113 -0
  29. data/lib/samagotchi/relay_watcher.rb +46 -0
  30. data/lib/samagotchi/reply_wait.rb +10 -1
  31. data/lib/samagotchi/self_report.rb +24 -4
  32. data/lib/samagotchi/session.rb +15 -2
  33. data/lib/samagotchi/session_manager.rb +5 -2
  34. data/lib/samagotchi/session_metrics.rb +18 -1
  35. data/lib/samagotchi/session_retention.rb +6 -1
  36. data/lib/samagotchi/sessions_command.rb +10 -4
  37. data/lib/samagotchi/terminal_ui/attach_launcher.rb +11 -2
  38. data/lib/samagotchi/terminal_ui/attached_loop.rb +88 -12
  39. data/lib/samagotchi/terminal_ui/event_renderer.rb +6 -1
  40. data/lib/samagotchi/terminal_ui/formatting.rb +7 -0
  41. data/lib/samagotchi/terminal_ui/question_prompt.rb +48 -2
  42. data/lib/samagotchi/terminal_ui/question_slot.rb +5 -1
  43. data/lib/samagotchi/terminal_ui.rb +11 -0
  44. data/lib/samagotchi/tool_declarations.rb +2 -2
  45. data/lib/samagotchi/tools/delegate_relay.rb +216 -0
  46. data/lib/samagotchi/tools/delegate_wait.rb +72 -6
  47. data/lib/samagotchi/tools/peers.rb +6 -4
  48. data/lib/samagotchi/turn_note.rb +30 -2
  49. data/lib/samagotchi/version.rb +1 -1
  50. data/lib/samagotchi/web/app.rb +24 -0
  51. data/lib/samagotchi/web/public/app.js +32 -11
  52. data/lib/samagotchi/web/public/index.html +9 -0
  53. data/lib/samagotchi/web/public/notify.js +18 -5
  54. data/lib/samagotchi/web/public/question_card.js +45 -3
  55. data/lib/samagotchi/web/public/question_cards.js +49 -1
  56. data/lib/samagotchi/web/public/sessions_list.js +16 -1
  57. data/lib/samagotchi/web/public/stage_model.js +8 -3
  58. data/lib/samagotchi/web/public/stage_view.js +7 -2
  59. data/lib/samagotchi/web/public/strip.js +8 -1
  60. data/lib/samagotchi/web/public/timing.js +22 -5
  61. data/lib/samagotchi/web/public/turn_events.js +9 -0
  62. data/lib/samagotchi/web/public/turn_view.js +38 -13
  63. metadata +7 -1
@@ -16,7 +16,7 @@ import { activityDiffHtml } from "./diff_view.js";
16
16
  import { escapeHtml, messageBodyHtml, noteHtml, normalize, steerRowHtml } from "./format.js";
17
17
  import { thumbsHtml } from "./images.js";
18
18
  import { shouldFollowScroll } from "./scroll.js";
19
- import { hookNoticeLabel } from "./turn_events.js";
19
+ import { emptyAnswerLine, hookNoticeLabel } from "./turn_events.js";
20
20
  import { cancelLineHtml, formatDuration, turnGroups, turnTimingText } from "./timing.js";
21
21
  import { createSentenceFeed } from "./sentences.js";
22
22
  import { createTicker } from "./thinking_ticker.js";
@@ -465,11 +465,15 @@ export function createTurnView({
465
465
  // +heldHeight+ (the live box's height, measured before the step closed) the
466
466
  // bubble is held at that height, scrolled to its tail, until answerReady()
467
467
  // (the rendered markdown landed) or 1.5 s reveals it.
468
- function promote(gen, { heldHeight = 0 } = {}) {
468
+ // +endNote+: the notice of a turn that ended with no answer (app.js
469
+ // builds it), placed where the answer bubble would go.
470
+ function promote(gen, { heldHeight = 0, endNote = null } = {}) {
469
471
  const entry = genEls.get(gen);
470
472
  const bubble = entry.text;
471
- bubble.className = "bubble output";
472
- if (!gen.text) bubble.textContent = "";
473
+ // No text (an empty answer): no bubble, not even an empty one left in
474
+ // the step.
475
+ if (gen.text) bubble.className = "bubble output";
476
+ else bubble.remove();
473
477
  // A hidden tab gets no pop: nobody sees it, and its timers are throttled.
474
478
  const holding = useHold && heldHeight > 0 && !!gen.text && !document.hidden;
475
479
  let holdHeight = 0;
@@ -512,11 +516,12 @@ export function createTurnView({
512
516
  }
513
517
  pieces.push(...cards);
514
518
  if (gen.text) pieces.push(bubble);
519
+ else if (endNote) pieces.push(endNote);
515
520
  placeAnswer(pieces, { plain: true, blockEl });
516
521
  blockEl = null;
517
522
  blockSummaryEl = null;
518
523
  } else {
519
- placeAnswer(gen.text ? [...cards, bubble] : cards, { plain: false, blockEl });
524
+ placeAnswer(gen.text ? [...cards, bubble] : [...cards, ...(endNote ? [endNote] : [])], { plain: false, blockEl });
520
525
  // The last step, left with its thinking only, reads "thinking" once.
521
526
  if (!gen.tools.length && gen.thinking) unwrapThinking(entry);
522
527
  }
@@ -524,8 +529,10 @@ export function createTurnView({
524
529
  }
525
530
 
526
531
  // Returns the cards that left the block (card.js leavesBlock), detached:
527
- // app.js puts them after the turn's end line.
528
- function turnEnded({ kind }) {
532
+ // app.js puts them after the turn's end line. +endNote+ (a completed turn
533
+ // with no answer): its notice, in the answer's place; left unplaced
534
+ // with no turn to end (app.js then shows it itself).
535
+ function turnEnded({ kind, endNote = null }) {
529
536
  // Once per turn: a second end (a re-dispatched event) would promote again.
530
537
  if (!turn || turn.ended) return [];
531
538
  const last = model.currentGen(turn);
@@ -541,7 +548,8 @@ export function createTurnView({
541
548
  const heldHeight = box ? box.getBoundingClientRect().height : 0;
542
549
  if (pendingSteers.length) flushSteers();
543
550
  apply(model.turnEnded(turn));
544
- if (promoting) promote(last, { heldHeight });
551
+ if (promoting) promote(last, { heldHeight, endNote });
552
+ else if (endNote) placeAnswer([endNote], { plain: false, blockEl });
545
553
  const kept = [];
546
554
  if (blockEl) {
547
555
  updateBlockSummary();
@@ -678,6 +686,13 @@ function reloadRowHtml(r, thumbs) {
678
686
  `${r.images?.length ? thumbs(r.images) : ""}${r.diff ? activityDiffHtml(r.diff) : ""}</div>`;
679
687
  }
680
688
 
689
+ // The notice of a turn that ended with no answer: a muted row like the
690
+ // loop's retry row, where the answer would be (live: promote()).
691
+ export const EMPTY_ANSWER_CLASS = "bubble hook-notice empty-answer";
692
+ function emptyAnswerHtml(item) {
693
+ return `<div class="${EMPTY_ANSWER_CLASS}">${escapeHtml(emptyAnswerLine(item))}</div>`;
694
+ }
695
+
681
696
  // A reloaded history in the turn view (renderHistory's object path): per
682
697
  // turn the prompt, a collapsed block of its steps when it had any (texts,
683
698
  // and with the server's parts the thinking, params and output, from the
@@ -687,8 +702,11 @@ function reloadRowHtml(r, thumbs) {
687
702
  export function turnHistoryHtml(items, timing, { thumbs }) {
688
703
  const timingHtml = (group) => {
689
704
  const record = group.record;
705
+ // Numbered by its place in the turn records, as live (a failed turn
706
+ // whose prompt went back still counts).
707
+ const at = (timing?.turnRecords || []).indexOf(record);
690
708
  return record && formatDuration(record.duration_ms)
691
- ? `<div class="turn-timing">${escapeHtml(turnTimingText(group.turnIndex + 1, record.duration_ms, { canceled: record.status === "canceled" }))}</div>`
709
+ ? `<div class="turn-timing">${escapeHtml(turnTimingText(at >= 0 ? at + 1 : group.turnIndex + 1, record.duration_ms, { canceled: record.status === "canceled" }))}</div>`
692
710
  : "";
693
711
  };
694
712
  // The timing line after the bubble, not in it: where the live view
@@ -705,9 +723,13 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
705
723
  const prompt = items[group.user];
706
724
  const parts = [`<div class="bubble user"${copySourceAttr(prompt, false)}><div class="user-message">${messageBodyHtml(prompt).body}${thumbs(prompt.images)}</div></div>`];
707
725
  const [only] = group.steps;
708
- const noBlock = !group.steps.length ||
709
- (group.steps.length === 1 && group.answer !== null && only.i === null && !only.tools.length && only.thinking);
710
- if (group.steps.length === 1 && group.answer !== null && only.i === null && !only.tools.length && only.thinking) {
726
+ const ended = group.answer !== null || group.emptyAnswer != null;
727
+ const textless = only && (only.i === null || !String(items[only.i].content ?? ""));
728
+ // A lone thinking step before an answer (or the notice of a turn with
729
+ // none): no block, as live.
730
+ const plainThinking = group.steps.length === 1 && ended && textless && !only.tools.length && only.thinking;
731
+ const noBlock = !group.steps.length || plainThinking;
732
+ if (plainThinking) {
711
733
  // A plain answer with thinking: the collapsed thinking block, as live.
712
734
  parts.push(`<details class="bubble thinking"><summary>thinking</summary><div class="thinking-body">${escapeHtml(only.thinking)}</div></details>`);
713
735
  } else if (group.steps.length) {
@@ -733,9 +755,12 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
733
755
  // after its steps and before its answer.
734
756
  parts.push((group.merged || []).map((i) =>
735
757
  `<div class="bubble user steered" data-user-state="steered"${items[i].step ? ` data-step="${Number(items[i].step)}"` : ""}${copySourceAttr(items[i], false)}><div class="user-message">${messageBodyHtml(items[i]).body}${thumbs(items[i].images)}</div><span class="state-badge">steered</span></div>`).join(""));
758
+ // A turn that ended with no answer: chi's muted notice where the answer
759
+ // would be, before the timing, as live.
760
+ if (group.emptyAnswer != null) parts.push(`${emptyAnswerHtml(items[group.emptyAnswer])}${timingHtml(group)}`);
736
761
  // A turn with no answer (canceled, or running: no record yet) shows its
737
762
  // timing after its prompt and steps, where the live view leaves it.
738
- if (group.answer === null) parts.push(timingHtml(group));
763
+ else if (group.answer === null) parts.push(timingHtml(group));
739
764
  else parts.push(answerHtml(items[group.answer], timingHtml(group)));
740
765
  // A canceled turn ends with its cancel line, as the live view left it.
741
766
  parts.push(cancelLineHtml(group.record, escapeHtml));
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.14.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -301,6 +301,7 @@ files:
301
301
  - lib/samagotchi/plugin/sessions.rb
302
302
  - lib/samagotchi/plugin/side_question.rb
303
303
  - lib/samagotchi/plugin/tool_result.rb
304
+ - lib/samagotchi/plugin_session_state.rb
304
305
  - lib/samagotchi/plugin_tasks.rb
305
306
  - lib/samagotchi/project_scope.rb
306
307
  - lib/samagotchi/prompt.rb
@@ -309,6 +310,10 @@ files:
309
310
  - lib/samagotchi/question_desk.rb
310
311
  - lib/samagotchi/recap_setup.rb
311
312
  - lib/samagotchi/recap_store.rb
313
+ - lib/samagotchi/relay.rb
314
+ - lib/samagotchi/relay_desk.rb
315
+ - lib/samagotchi/relay_verifier.rb
316
+ - lib/samagotchi/relay_watcher.rb
312
317
  - lib/samagotchi/reminder_queue.rb
313
318
  - lib/samagotchi/reminder_store.rb
314
319
  - lib/samagotchi/reply_wait.rb
@@ -365,6 +370,7 @@ files:
365
370
  - lib/samagotchi/tools/builtins.rb
366
371
  - lib/samagotchi/tools/cancel_reminder.rb
367
372
  - lib/samagotchi/tools/delegate.rb
373
+ - lib/samagotchi/tools/delegate_relay.rb
368
374
  - lib/samagotchi/tools/delegate_result.rb
369
375
  - lib/samagotchi/tools/delegate_wait.rb
370
376
  - lib/samagotchi/tools/edit.rb