samagotchi 0.61.0 → 0.62.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -1
- data/docs/architecture.md +3 -0
- data/docs/cli.md +22 -8
- data/docs/configuration.md +32 -19
- data/docs/context.md +8 -0
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +8 -0
- data/docs/internals/engine.md +24 -11
- data/docs/internals/prompt-caching.md +6 -1
- data/docs/plugins.md +42 -3
- data/docs/sessions.md +4 -4
- data/docs/testing.md +4 -1
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +70 -14
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +5 -1
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +58 -3
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -4
- data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +9 -0
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/kernel_loop.rb +7 -16
- data/lib/samagotchi/llm/chat_loop.rb +23 -15
- data/lib/samagotchi/plugin/api.rb +36 -18
- data/lib/samagotchi/reasoning_history.rb +22 -4
- data/lib/samagotchi/row_refs.rb +126 -0
- data/lib/samagotchi/session_metrics.rb +24 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +26 -3
- data/lib/samagotchi/tool_activity.rb +8 -4
- data/lib/samagotchi/tool_response.rb +5 -4
- data/lib/samagotchi/tool_row_fields.rb +4 -2
- data/lib/samagotchi/tool_runner.rb +16 -3
- data/lib/samagotchi/tools/builtins.rb +9 -2
- data/lib/samagotchi/tools/execute.rb +15 -3
- data/lib/samagotchi/tools/peers.rb +3 -2
- data/lib/samagotchi/tools/registry.rb +5 -3
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +14 -1
- data/lib/samagotchi/web/message_parts.rb +17 -9
- data/lib/samagotchi/web/public/activity.js +4 -1
- data/lib/samagotchi/web/public/app.js +59 -15
- data/lib/samagotchi/web/public/chains.js +18 -20
- data/lib/samagotchi/web/public/context_chips.js +92 -64
- data/lib/samagotchi/web/public/families_view.js +51 -39
- data/lib/samagotchi/web/public/index.html +61 -10
- data/lib/samagotchi/web/public/keep_focus.js +24 -0
- data/lib/samagotchi/web/public/lists_view.js +13 -13
- data/lib/samagotchi/web/public/llm_context_chip.js +55 -34
- data/lib/samagotchi/web/public/question_card.js +11 -0
- data/lib/samagotchi/web/public/question_cards.js +60 -5
- data/lib/samagotchi/web/public/ref_menu.js +18 -5
- data/lib/samagotchi/web/public/refs.js +113 -15
- data/lib/samagotchi/web/public/scroll.js +42 -0
- data/lib/samagotchi/web/public/stage_model.js +22 -3
- data/lib/samagotchi/web/public/stage_view.js +299 -70
- data/lib/samagotchi/web/public/timing.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +5 -4
- data/lib/samagotchi/web/public/turn_model.js +49 -0
- data/lib/samagotchi/web/public/turn_view.js +104 -10
- data/lib/samagotchi/worker.rb +3 -2
- data/lib/samagotchi/worker_inbound.rb +5 -3
- metadata +3 -1
|
@@ -152,6 +152,20 @@ export function notice(turn, { hook, text, level } = {}) {
|
|
|
152
152
|
return { gen };
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// The whole text of the reply that asked the user (a question or an
|
|
156
|
+
// approval blocks its call): the last gen's text, trimmed, while one of its
|
|
157
|
+
// calls runs and the turn goes on; else "". Only that step's text: a hook's
|
|
158
|
+
// question (asked before its call's row, or between steps) has no running
|
|
159
|
+
// row there, so it gets none rather than another step's (or the answer's)
|
|
160
|
+
// text. The stage shows it while the card waits; the card keeps it once
|
|
161
|
+
// answered.
|
|
162
|
+
export function askingText(turn) {
|
|
163
|
+
if (!turn || turn.ended) return "";
|
|
164
|
+
const gen = turn.gens[turn.gens.length - 1];
|
|
165
|
+
if (!gen?.text || !gen.tools.some((row) => row.status === "running")) return "";
|
|
166
|
+
return gen.text.trim();
|
|
167
|
+
}
|
|
168
|
+
|
|
155
169
|
// The turn is over (completed, canceled, failed, the worker gone).
|
|
156
170
|
export function turnEnded(turn) {
|
|
157
171
|
const closed = closeCurrent(turn);
|
|
@@ -215,6 +229,41 @@ export function genLabelParts(gen) {
|
|
|
215
229
|
return { head: "thinking", calls: "", repeats: false };
|
|
216
230
|
}
|
|
217
231
|
|
|
232
|
+
// A step that said something (its text is not the turn's answer): a row of
|
|
233
|
+
// its own in a finished block's outline.
|
|
234
|
+
export function isNarrated(gen) {
|
|
235
|
+
return !gen.answer && !!String(gen.text || "").trim();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// A finished block's outline (+gens+: its steps in order, as the block holds
|
|
239
|
+
// them): each narrated step a row, each run of the others between (tool-only,
|
|
240
|
+
// thinking-only) one fold ({fold: [indices]}), so every mid-turn text's first
|
|
241
|
+
// line reads without a click and the steps that said nothing take one line.
|
|
242
|
+
// [] when no step said anything: the block stays closed, as before.
|
|
243
|
+
// A last run of one thinking-only step stays a row (no "· 1 step" at the end).
|
|
244
|
+
export function outlineRuns(gens) {
|
|
245
|
+
if (!gens.some(isNarrated)) return [];
|
|
246
|
+
const runs = [];
|
|
247
|
+
gens.forEach((gen, i) => {
|
|
248
|
+
if (isNarrated(gen)) return runs.push({ step: i });
|
|
249
|
+
const last = runs[runs.length - 1];
|
|
250
|
+
if (last?.fold) last.fold.push(i);
|
|
251
|
+
else runs.push({ fold: [i] });
|
|
252
|
+
});
|
|
253
|
+
// A turn doesn't end on a "1 step" fold for a lone thinking-only step
|
|
254
|
+
// (the answer's own thinking): it stays a row, its "thinking" label.
|
|
255
|
+
const last = runs[runs.length - 1];
|
|
256
|
+
const lone = last.fold?.length === 1 && gens[last.fold[0]];
|
|
257
|
+
if (lone && lone.tools.length === 0) runs[runs.length - 1] = { step: last.fold[0] };
|
|
258
|
+
return runs;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// A finished turn's steps as its block holds them: the answer's step left it
|
|
262
|
+
// when the answer was all it had (turn_view.js promote).
|
|
263
|
+
export function blockGens(turn) {
|
|
264
|
+
return turn.gens.filter((gen) => !(gen.answer && !gen.thinking && gen.tools.length === 0));
|
|
265
|
+
}
|
|
266
|
+
|
|
218
267
|
// The block's summary line: "working · 3 steps · 8 tool calls · execute ×7 · …"
|
|
219
268
|
// while it runs, without the prefix after; the tally (tally.js) from 3 calls.
|
|
220
269
|
export function blockSummary(turn, { running = false } = {}) {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// removed at the end: the answer bubble takes its place.
|
|
23
23
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
24
24
|
import { setAnswerRefs } from "./answer_refs.js";
|
|
25
|
+
import { bindRowRefs } from "./refs.js";
|
|
25
26
|
import { leavesBlock } from "./card.js";
|
|
26
27
|
import { copySourceAttr } from "./copy.js";
|
|
27
28
|
import { escapeHtml, failedTurnText, messageBodyHtml, noteHtml, normalize, sourceLabel, steerRowHtml } from "./format.js";
|
|
@@ -116,6 +117,67 @@ function rawNodes(markup) {
|
|
|
116
117
|
return t.content;
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
// ---- A finished block's outline ----
|
|
121
|
+
|
|
122
|
+
// A finished block as its outline (turn_model.js outlineRuns of +gens+, its
|
|
123
|
+
// steps in order): open, each narrated step a one-line row as before, each
|
|
124
|
+
// run of steps that said nothing hidden behind one "N steps" fold that
|
|
125
|
+
// shows them. The live turn's end and a reload call it alike. The folds sit
|
|
126
|
+
// between the steps, not in them (`:scope > .gen` still counts steps), each
|
|
127
|
+
// before its run's first step and before that step's lit marker comment, so
|
|
128
|
+
// a later render of the step leaves it alone. False when there is nothing
|
|
129
|
+
// to outline (no step said anything): the block stays as it is.
|
|
130
|
+
export function outlineBlock(blockEl, gens) {
|
|
131
|
+
const runs = model.outlineRuns(gens);
|
|
132
|
+
const steps = blockEl ? [...blockEl.querySelectorAll(":scope > .gen")] : [];
|
|
133
|
+
if (!runs.length || steps.length !== gens.length) return false;
|
|
134
|
+
blockEl.classList.add("outline");
|
|
135
|
+
// A folded step → its fold's opener: a ref's "show in turn" (refs.js
|
|
136
|
+
// showInTurn dispatches ref:reveal on the row before it scrolls) unfolds
|
|
137
|
+
// the run its row is in, or the row stays hidden.
|
|
138
|
+
const unfold = new Map();
|
|
139
|
+
for (const { fold } of runs) {
|
|
140
|
+
if (!fold) continue;
|
|
141
|
+
const folded = fold.map((i) => steps[i]);
|
|
142
|
+
folded.forEach((el) => el.classList.add("folded"));
|
|
143
|
+
const marker = document.createElement("div");
|
|
144
|
+
marker.className = "outline-fold";
|
|
145
|
+
marker.setAttribute("role", "button");
|
|
146
|
+
marker.tabIndex = 0;
|
|
147
|
+
marker.setAttribute("aria-expanded", "false");
|
|
148
|
+
marker.textContent = `${fold.length} step${fold.length === 1 ? "" : "s"}`;
|
|
149
|
+
const toggle = () => {
|
|
150
|
+
const open = marker.classList.toggle("open");
|
|
151
|
+
marker.setAttribute("aria-expanded", String(open));
|
|
152
|
+
folded.forEach((el) => el.classList.toggle("unfolded", open));
|
|
153
|
+
};
|
|
154
|
+
folded.forEach((el) => unfold.set(el, () => { if (!marker.classList.contains("open")) toggle(); }));
|
|
155
|
+
marker.addEventListener("click", toggle);
|
|
156
|
+
marker.addEventListener("keydown", (e) => {
|
|
157
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
toggle();
|
|
160
|
+
});
|
|
161
|
+
const first = folded[0];
|
|
162
|
+
const anchor = first.previousSibling?.nodeType === 8 ? first.previousSibling : first;
|
|
163
|
+
anchor.before(marker);
|
|
164
|
+
}
|
|
165
|
+
blockEl.addEventListener("ref:reveal", (e) => {
|
|
166
|
+
const step = e.target.closest?.(".gen.folded");
|
|
167
|
+
if (step?.parentElement === blockEl) unfold.get(step)?.();
|
|
168
|
+
});
|
|
169
|
+
blockEl.open = true;
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Reloaded tool rows (turn_html.js toolRowHtml) as nodes, each row's
|
|
174
|
+
// plugin refs bound to its title (refs.js bindRowRefs).
|
|
175
|
+
function rowNodes(rows, thumbs) {
|
|
176
|
+
const nodes = rawNodes(rows.map((r) => toolRowHtml(r, { thumbs })).join(""));
|
|
177
|
+
[...nodes.children].forEach((el, i) => bindRowRefs(el, rows[i]));
|
|
178
|
+
return nodes;
|
|
179
|
+
}
|
|
180
|
+
|
|
119
181
|
// ---- The live view ----
|
|
120
182
|
|
|
121
183
|
// The parts of a drawn tool row a user may have changed (the command's
|
|
@@ -445,12 +507,16 @@ export function createTurnView({
|
|
|
445
507
|
const inner = toolRowInnerHtml(r.row, { thumbs: rowThumbs });
|
|
446
508
|
if (!el) {
|
|
447
509
|
el = htmlElement(toolRowHtml(r.row, { thumbs: rowThumbs }));
|
|
510
|
+
bindRowRefs(el, r.row);
|
|
448
511
|
rowHtml.set(el, inner);
|
|
449
512
|
entry.rows.set(r.row.key, el);
|
|
450
513
|
addItem(entry, el);
|
|
451
514
|
} else {
|
|
452
515
|
if (!r.gen.text) entry.placeholder = false;
|
|
453
|
-
if (rowHtml.get(el) !== inner)
|
|
516
|
+
if (rowHtml.get(el) !== inner) {
|
|
517
|
+
redrawRow(el, inner);
|
|
518
|
+
bindRowRefs(el, r.row);
|
|
519
|
+
}
|
|
454
520
|
rowHtml.set(el, inner);
|
|
455
521
|
// A late result in a closed step: its label and failed mark.
|
|
456
522
|
draw(entry);
|
|
@@ -603,7 +669,11 @@ export function createTurnView({
|
|
|
603
669
|
if (blockEl) {
|
|
604
670
|
updateBlockSummary();
|
|
605
671
|
blockEl.classList.add("done");
|
|
606
|
-
|
|
672
|
+
// The outline (turn_model.js outlineRuns): open on its narrated steps'
|
|
673
|
+
// rows, as a reload draws it; a block with nothing said closes.
|
|
674
|
+
if (newestFirst) chronological();
|
|
675
|
+
const outlined = outlineBlock(blockEl, model.blockGens(turn));
|
|
676
|
+
if (!blockToggled) blockEl.open = outlined;
|
|
607
677
|
for (const el of blockEl.querySelectorAll(".plugin-card")) {
|
|
608
678
|
if (leavesBlock({ warn: el.classList.contains("warn"), kind })) kept.push(el);
|
|
609
679
|
}
|
|
@@ -615,6 +685,20 @@ export function createTurnView({
|
|
|
615
685
|
return kept;
|
|
616
686
|
}
|
|
617
687
|
|
|
688
|
+
// The steps back in chronological order after newestFirst (a no-op
|
|
689
|
+
// otherwise); a fold (outlineBlock) stays before its run's first step.
|
|
690
|
+
function chronological() {
|
|
691
|
+
if (!blockEl || !turn) return;
|
|
692
|
+
for (const gen of turn.gens) {
|
|
693
|
+
const entry = genEls.get(gen);
|
|
694
|
+
if (entry?.el.parentNode !== blockEl) continue;
|
|
695
|
+
const nodes = stepNodes(entry);
|
|
696
|
+
const fold = nodes[0].previousSibling;
|
|
697
|
+
if (fold?.classList?.contains("outline-fold")) nodes.unshift(fold);
|
|
698
|
+
blockEl.append(...nodes);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
618
702
|
function reset() {
|
|
619
703
|
hold.finish();
|
|
620
704
|
turn = null;
|
|
@@ -712,17 +796,14 @@ export function createTurnView({
|
|
|
712
796
|
// block, a step's elements by index, and the steps back in
|
|
713
797
|
// chronological order after newestFirst.
|
|
714
798
|
current() { return { turn, blockEl }; },
|
|
799
|
+
// The running turn's asking reply (turn_model.js askingText): what a
|
|
800
|
+
// question or approval card keeps once answered.
|
|
801
|
+
askingText: () => model.askingText(turn),
|
|
715
802
|
step(i) {
|
|
716
803
|
const entry = turn && genEls.get(turn.gens[i]);
|
|
717
804
|
return entry ? { el: entry.el, summary: entry.el.querySelector(":scope > summary"), thinkingWrap: thinkingWrap(entry) } : null;
|
|
718
805
|
},
|
|
719
|
-
chronological
|
|
720
|
-
if (!blockEl || !turn) return;
|
|
721
|
-
for (const gen of turn.gens) {
|
|
722
|
-
const entry = genEls.get(gen);
|
|
723
|
-
if (entry?.el.parentNode === blockEl) blockEl.append(...stepNodes(entry));
|
|
724
|
-
}
|
|
725
|
-
},
|
|
806
|
+
chronological,
|
|
726
807
|
};
|
|
727
808
|
}
|
|
728
809
|
|
|
@@ -737,6 +818,14 @@ function defaultPlaceAnswer(appendToHistory) {
|
|
|
737
818
|
|
|
738
819
|
// ---- A reload ----
|
|
739
820
|
|
|
821
|
+
// The text of a reloaded block's step +iteration+ (1-based, as the snapshot's
|
|
822
|
+
// cards count), "" when it has none: a reloaded question card's asking
|
|
823
|
+
// reply.
|
|
824
|
+
export function stepText(block, iteration) {
|
|
825
|
+
const step = block?.querySelectorAll(":scope > .gen")[Number(iteration) - 1];
|
|
826
|
+
return step?.querySelector(":scope > .gen-text")?.textContent || "";
|
|
827
|
+
}
|
|
828
|
+
|
|
740
829
|
// A re-rendered failed turn's line (a .bubble.cancel, as live's
|
|
741
830
|
// addEndBubble), in turn_failed's words (failedTurnText) from what the turn
|
|
742
831
|
// record kept of it (SessionMetrics' failure: summary, message,
|
|
@@ -806,6 +895,8 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
|
|
|
806
895
|
};
|
|
807
896
|
const steerHtml = (i, bubble) => steerRowHtml({ source: items[i].source, text: items[i].content }, { bubble });
|
|
808
897
|
const parts = [];
|
|
898
|
+
// Each block's steps, in order: its outline once the nodes exist.
|
|
899
|
+
const outlines = [];
|
|
809
900
|
for (const group of turnGroups(items, timing)) {
|
|
810
901
|
if (group.kind === "note") { parts.push(`<div class="bubble note">${noteHtml(items[group.i])}</div>`); continue; }
|
|
811
902
|
if (group.kind === "steer") { parts.push(steerHtml(group.i, true)); continue; }
|
|
@@ -827,9 +918,10 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
|
|
|
827
918
|
const steps = gens.map((gen, k) => stepTemplate(gen, {
|
|
828
919
|
// The steers the step answered first, where the live rows went.
|
|
829
920
|
steers: (group.steps[k].steers || []).map((i) => rawNodes(steerHtml(i, false))),
|
|
830
|
-
items: gen.tools.length ? [
|
|
921
|
+
items: gen.tools.length ? [rowNodes(gen.tools, thumbs)] : [],
|
|
831
922
|
}));
|
|
832
923
|
parts.push(html`<details class="turn-work done"><summary>${model.blockSummary({ gens, ended: true })}</summary>${steps}</details>`);
|
|
924
|
+
outlines.push(gens);
|
|
833
925
|
}
|
|
834
926
|
// Steers with no step to sit in, or in a turn that shows no block: on
|
|
835
927
|
// their own after the prompt.
|
|
@@ -856,6 +948,8 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
|
|
|
856
948
|
parts.push(failedLineHtml(group.record));
|
|
857
949
|
}
|
|
858
950
|
const out = fragmentOf(parts);
|
|
951
|
+
// A finished turn's block as the live end leaves it: its outline, open.
|
|
952
|
+
[...out.children].filter((el) => el.matches(".turn-work")).forEach((block, i) => outlineBlock(block, outlines[i] || []));
|
|
859
953
|
// Each answer bubble knows its message's refs (answer_refs.js; the
|
|
860
954
|
// page pairs them after the render), the index stays out of the DOM.
|
|
861
955
|
for (const bubble of out.querySelectorAll("[data-answer-at]")) {
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -1077,8 +1077,9 @@ module Samagotchi
|
|
|
1077
1077
|
# A wake turn starts with its first line, whoever sent it.
|
|
1078
1078
|
lines[0] = lines[0].with(mark: mark) if mark && lines.any?
|
|
1079
1079
|
# The turn so far, with what this boundary merges (its input files
|
|
1080
|
-
# are gone now), on disk at most every TurnCheckpoint::INTERVAL
|
|
1081
|
-
|
|
1080
|
+
# are gone now), on disk at most every TurnCheckpoint::INTERVAL;
|
|
1081
|
+
# its metrics so far with it, so a turn a KILL ends keeps them.
|
|
1082
|
+
@engine.checkpoint_metrics if @checkpoint.boundary(@engine.live_conversation, merging: lines, turn_id: @turn_id)
|
|
1082
1083
|
lines
|
|
1083
1084
|
end
|
|
1084
1085
|
end
|
|
@@ -201,9 +201,11 @@ module Samagotchi
|
|
|
201
201
|
|
|
202
202
|
private
|
|
203
203
|
|
|
204
|
-
# +unless_stopped+:
|
|
205
|
-
#
|
|
206
|
-
#
|
|
204
|
+
# +unless_stopped+: skipped when the session was stopped meanwhile (chi
|
|
205
|
+
# sessions stop, from another process: its marker file,
|
|
206
|
+
# Session.mark_stopped, which a load reads over the saved status). The
|
|
207
|
+
# save couldn't hide the stop; it is left out so the file stays as the
|
|
208
|
+
# stop found it, the worker's idle status unwritten.
|
|
207
209
|
def save(at, unless_stopped: false)
|
|
208
210
|
self.class.save_or_log(at) do
|
|
209
211
|
@session.save(state_dir: @state_dir) unless unless_stopped && @stopped.call
|
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.
|
|
4
|
+
version: 0.62.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -416,6 +416,7 @@ files:
|
|
|
416
416
|
- lib/samagotchi/reminder_queue.rb
|
|
417
417
|
- lib/samagotchi/reminder_store.rb
|
|
418
418
|
- lib/samagotchi/reply_wait.rb
|
|
419
|
+
- lib/samagotchi/row_refs.rb
|
|
419
420
|
- lib/samagotchi/sampling_settings.rb
|
|
420
421
|
- lib/samagotchi/self_report.rb
|
|
421
422
|
- lib/samagotchi/send_command.rb
|
|
@@ -557,6 +558,7 @@ files:
|
|
|
557
558
|
- lib/samagotchi/web/public/index.html
|
|
558
559
|
- lib/samagotchi/web/public/info_bar.js
|
|
559
560
|
- lib/samagotchi/web/public/init_row.js
|
|
561
|
+
- lib/samagotchi/web/public/keep_focus.js
|
|
560
562
|
- lib/samagotchi/web/public/kept_commands.js
|
|
561
563
|
- lib/samagotchi/web/public/level_chip.js
|
|
562
564
|
- lib/samagotchi/web/public/lists_view.js
|