samagotchi 0.61.0 → 0.63.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 +91 -1
- data/README.md +4 -1
- data/bin/chi +3 -2
- data/docs/architecture.md +3 -0
- data/docs/cli.md +26 -9
- data/docs/configuration.md +33 -20
- data/docs/context.md +21 -3
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +14 -3
- data/docs/internals/engine.md +24 -11
- data/docs/internals/prompt-caching.md +6 -1
- data/docs/memory.md +1 -1
- data/docs/plugins.md +131 -5
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +4 -4
- data/docs/sub-agent.md +6 -0
- data/docs/testing.md +10 -4
- data/lib/samagotchi/answer_refs.rb +18 -26
- data/lib/samagotchi/bridge.rb +23 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +22 -0
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +406 -0
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +104 -0
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +66 -0
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +60 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -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/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +17 -6
- 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 +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/context_absorber.rb +25 -6
- data/lib/samagotchi/engine.rb +9 -0
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/idle_recap.rb +2 -3
- data/lib/samagotchi/image_store.rb +8 -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/plugin/attached_context.rb +7 -4
- data/lib/samagotchi/reasoning_history.rb +22 -4
- data/lib/samagotchi/recap_setup.rb +3 -3
- data/lib/samagotchi/row_refs.rb +125 -0
- data/lib/samagotchi/session.rb +12 -0
- data/lib/samagotchi/session_metrics.rb +24 -2
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +6 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +26 -9
- data/lib/samagotchi/terminal_ui/formatting.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +57 -14
- 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 +37 -7
- 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 +20 -5
- data/lib/samagotchi/web/message_parts.rb +17 -9
- data/lib/samagotchi/web/public/activity.js +6 -3
- data/lib/samagotchi/web/public/app.js +106 -106
- data/lib/samagotchi/web/public/card.js +73 -23
- data/lib/samagotchi/web/public/chains.js +24 -24
- data/lib/samagotchi/web/public/command_view.js +39 -35
- data/lib/samagotchi/web/public/context_chips.js +92 -64
- data/lib/samagotchi/web/public/copy.js +7 -15
- data/lib/samagotchi/web/public/diff_view.js +20 -23
- data/lib/samagotchi/web/public/families_view.js +51 -39
- data/lib/samagotchi/web/public/format.js +0 -48
- data/lib/samagotchi/web/public/images.js +12 -16
- 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 +63 -7
- data/lib/samagotchi/web/public/read_view.js +10 -9
- data/lib/samagotchi/web/public/ref_menu.js +18 -5
- data/lib/samagotchi/web/public/refs.js +113 -15
- data/lib/samagotchi/web/public/scope.js +23 -0
- 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 -8
- data/lib/samagotchi/web/public/turn_html.js +122 -61
- data/lib/samagotchi/web/public/turn_model.js +49 -0
- data/lib/samagotchi/web/public/turn_view.js +167 -116
- data/lib/samagotchi/web/public/vendor/directive.js +8 -0
- data/lib/samagotchi/web/public/vendor/directives/unsafe-html.js +8 -0
- data/lib/samagotchi/worker.rb +49 -32
- data/lib/samagotchi/worker_inbound.rb +17 -14
- metadata +10 -1
|
@@ -22,14 +22,17 @@
|
|
|
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
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import { thumbsHtml } from "./images.js";
|
|
27
|
+
import { failedTurnText, normalize, sourceLabel } from "./format.js";
|
|
28
|
+
import { thumbsTemplate } from "./images.js";
|
|
29
29
|
import { shouldFollowScroll } from "./scroll.js";
|
|
30
30
|
import { hookNoticeLabel } from "./turn_events.js";
|
|
31
|
-
import {
|
|
32
|
-
|
|
31
|
+
import {
|
|
32
|
+
answerBubbleTemplate, cancelLineTemplate, commandBubbleTemplate, elementOf, emptyAnswerTemplate, noteBubbleTemplate, shellCommandMessage,
|
|
33
|
+
steerRowTemplate, toolRowInnerTemplate, toolRowTemplate, userBubbleTemplate,
|
|
34
|
+
} from "./turn_html.js";
|
|
35
|
+
import { formatDuration, turnGroups, turnTimingText } from "./timing.js";
|
|
33
36
|
import { createSentenceFeed } from "./sentences.js";
|
|
34
37
|
import { createTicker } from "./thinking_ticker.js";
|
|
35
38
|
import * as model from "./turn_model.js";
|
|
@@ -109,41 +112,68 @@ function nodesOf(template) {
|
|
|
109
112
|
return frag;
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
// ---- A finished block's outline ----
|
|
116
|
+
|
|
117
|
+
// A finished block as its outline (turn_model.js outlineRuns of +gens+, its
|
|
118
|
+
// steps in order): open, each narrated step a one-line row as before, each
|
|
119
|
+
// run of steps that said nothing hidden behind one "N steps" fold that
|
|
120
|
+
// shows them. The live turn's end and a reload call it alike. The folds sit
|
|
121
|
+
// between the steps, not in them (`:scope > .gen` still counts steps), each
|
|
122
|
+
// before its run's first step and before that step's lit marker comment, so
|
|
123
|
+
// a later render of the step leaves it alone. False when there is nothing
|
|
124
|
+
// to outline (no step said anything): the block stays as it is.
|
|
125
|
+
export function outlineBlock(blockEl, gens) {
|
|
126
|
+
const runs = model.outlineRuns(gens);
|
|
127
|
+
const steps = blockEl ? [...blockEl.querySelectorAll(":scope > .gen")] : [];
|
|
128
|
+
if (!runs.length || steps.length !== gens.length) return false;
|
|
129
|
+
blockEl.classList.add("outline");
|
|
130
|
+
// A folded step → its fold's opener: a ref's "show in turn" (refs.js
|
|
131
|
+
// showInTurn dispatches ref:reveal on the row before it scrolls) unfolds
|
|
132
|
+
// the run its row is in, or the row stays hidden.
|
|
133
|
+
const unfold = new Map();
|
|
134
|
+
for (const { fold } of runs) {
|
|
135
|
+
if (!fold) continue;
|
|
136
|
+
const folded = fold.map((i) => steps[i]);
|
|
137
|
+
folded.forEach((el) => el.classList.add("folded"));
|
|
138
|
+
const marker = document.createElement("div");
|
|
139
|
+
marker.className = "outline-fold";
|
|
140
|
+
marker.setAttribute("role", "button");
|
|
141
|
+
marker.tabIndex = 0;
|
|
142
|
+
marker.setAttribute("aria-expanded", "false");
|
|
143
|
+
marker.textContent = `${fold.length} step${fold.length === 1 ? "" : "s"}`;
|
|
144
|
+
const toggle = () => {
|
|
145
|
+
const open = marker.classList.toggle("open");
|
|
146
|
+
marker.setAttribute("aria-expanded", String(open));
|
|
147
|
+
folded.forEach((el) => el.classList.toggle("unfolded", open));
|
|
148
|
+
};
|
|
149
|
+
folded.forEach((el) => unfold.set(el, () => { if (!marker.classList.contains("open")) toggle(); }));
|
|
150
|
+
marker.addEventListener("click", toggle);
|
|
151
|
+
marker.addEventListener("keydown", (e) => {
|
|
152
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
toggle();
|
|
155
|
+
});
|
|
156
|
+
const first = folded[0];
|
|
157
|
+
const anchor = first.previousSibling?.nodeType === 8 ? first.previousSibling : first;
|
|
158
|
+
anchor.before(marker);
|
|
159
|
+
}
|
|
160
|
+
blockEl.addEventListener("ref:reveal", (e) => {
|
|
161
|
+
const step = e.target.closest?.(".gen.folded");
|
|
162
|
+
if (step?.parentElement === blockEl) unfold.get(step)?.();
|
|
163
|
+
});
|
|
164
|
+
blockEl.open = true;
|
|
165
|
+
return true;
|
|
117
166
|
}
|
|
118
167
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const childWith = (el, cls) => [...el.children].find((c) => c.classList.contains(cls));
|
|
127
|
-
|
|
128
|
-
// A live tool row drawn again from its activity row (status, title,
|
|
129
|
-
// output): the reloaded row's markup (toolRowInnerHtml), the kept parts
|
|
130
|
-
// and the stop-task button carried over. +html+ is that markup.
|
|
131
|
-
function redrawRow(el, markup) {
|
|
132
|
-
const fresh = htmlElement(`<div>${markup}</div>`);
|
|
133
|
-
for (const cls of KEPT_PARTS) {
|
|
134
|
-
const old = childWith(el, cls);
|
|
135
|
-
const drawn = childWith(fresh, cls);
|
|
136
|
-
if (old && drawn) drawn.replaceWith(old);
|
|
137
|
-
}
|
|
138
|
-
const stop = childWith(el, "task-stop");
|
|
139
|
-
el.replaceChildren(...fresh.childNodes);
|
|
140
|
-
if (stop) rowAnchor(el).after(stop);
|
|
168
|
+
// Reloaded tool rows (turn_html.js toolRowTemplate) as nodes, each row's
|
|
169
|
+
// plugin refs bound to its title (refs.js bindRowRefs).
|
|
170
|
+
function rowNodes(rows, thumbs) {
|
|
171
|
+
const nodes = nodesOf(rows.map((r) => toolRowTemplate(r, { thumbs })));
|
|
172
|
+
[...nodes.children].forEach((el, i) => bindRowRefs(el, rows[i]));
|
|
173
|
+
return nodes;
|
|
141
174
|
}
|
|
142
175
|
|
|
143
|
-
//
|
|
144
|
-
function rowAnchor(el) {
|
|
145
|
-
return childWith(el, "activity-params") || childWith(el, "activity-tool");
|
|
146
|
-
}
|
|
176
|
+
// ---- The live view ----
|
|
147
177
|
|
|
148
178
|
// The container seams (the stage view composes this view, stage_view.js):
|
|
149
179
|
// blockHost(el) puts the turn's block in the page (default: the history);
|
|
@@ -423,10 +453,11 @@ export function createTurnView({
|
|
|
423
453
|
onChange();
|
|
424
454
|
}
|
|
425
455
|
|
|
426
|
-
// A tool row is the reloaded row's
|
|
427
|
-
//
|
|
428
|
-
const
|
|
429
|
-
|
|
456
|
+
// A live tool row is its own element, the reloaded row's template
|
|
457
|
+
// (turn_html.js toolRowInnerTemplate) rendered into it on each event.
|
|
458
|
+
const rowThumbs = (images) => thumbsTemplate(sessionId(), images);
|
|
459
|
+
// A row element → its stop-task button (syncTaskStop).
|
|
460
|
+
const rowStops = new WeakMap();
|
|
430
461
|
|
|
431
462
|
// A row, notice or card into the step's activity; a step that wrote no
|
|
432
463
|
// narration before it drops the "…".
|
|
@@ -442,39 +473,48 @@ export function createTurnView({
|
|
|
442
473
|
apply(r);
|
|
443
474
|
const entry = genEls.get(r.gen);
|
|
444
475
|
let el = entry.rows.get(r.row.key);
|
|
445
|
-
const inner = toolRowInnerHtml(r.row, { thumbs: rowThumbs });
|
|
446
476
|
if (!el) {
|
|
447
|
-
el =
|
|
448
|
-
|
|
477
|
+
el = document.createElement("div");
|
|
478
|
+
el.className = "activity-row";
|
|
479
|
+
el.dataset.key = r.row.key;
|
|
449
480
|
entry.rows.set(r.row.key, el);
|
|
481
|
+
drawRow(el, r.row);
|
|
450
482
|
addItem(entry, el);
|
|
451
483
|
} else {
|
|
452
484
|
if (!r.gen.text) entry.placeholder = false;
|
|
453
|
-
|
|
454
|
-
rowHtml.set(el, inner);
|
|
485
|
+
drawRow(el, r.row);
|
|
455
486
|
// A late result in a closed step: its label and failed mark.
|
|
456
487
|
draw(entry);
|
|
457
488
|
}
|
|
458
|
-
syncTaskStop(el, r.row);
|
|
459
489
|
updateBlockSummary();
|
|
460
490
|
onChange();
|
|
461
491
|
}
|
|
462
492
|
|
|
463
|
-
// The
|
|
464
|
-
//
|
|
493
|
+
// The row rendered again (lit writes only what changed: a toggled raw
|
|
494
|
+
// command, an opened diff or read and the thumbs stay), its stop-task
|
|
495
|
+
// button in its slot, its plugin refs bound to its title (refs.js).
|
|
496
|
+
function drawRow(el, row) {
|
|
497
|
+
render(toolRowInnerTemplate(row, { thumbs: rowThumbs, stop: syncTaskStop(el, row) }), el);
|
|
498
|
+
bindRowRefs(el, row);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// The stop-task button of a running task_wait's row (after its title),
|
|
502
|
+
// nothing once the row completes.
|
|
465
503
|
function syncTaskStop(el, row) {
|
|
466
504
|
const taskId = taskStop?.target(row);
|
|
467
|
-
|
|
505
|
+
let btn = rowStops.get(el);
|
|
468
506
|
if (!taskId) {
|
|
469
|
-
|
|
470
|
-
return;
|
|
507
|
+
rowStops.delete(el);
|
|
508
|
+
return nothing;
|
|
471
509
|
}
|
|
472
510
|
if (btn) {
|
|
473
511
|
btn.disabled = taskStop.busy(taskId);
|
|
474
|
-
return;
|
|
512
|
+
return btn;
|
|
475
513
|
}
|
|
476
514
|
const command = () => taskCommand(turn?.gens, taskId);
|
|
477
|
-
|
|
515
|
+
btn = taskStopButton(taskStop, taskId, { command });
|
|
516
|
+
rowStops.set(el, btn);
|
|
517
|
+
return btn;
|
|
478
518
|
}
|
|
479
519
|
|
|
480
520
|
// A hook's line (hook_notice) as a row of the current step, above the row
|
|
@@ -494,7 +534,7 @@ export function createTurnView({
|
|
|
494
534
|
// that ends first puts it in its last step). False with no running turn.
|
|
495
535
|
function steer(data) {
|
|
496
536
|
if (!turn || turn.ended) return false;
|
|
497
|
-
pendingSteers.push(
|
|
537
|
+
pendingSteers.push(elementOf(steerRowTemplate(data)));
|
|
498
538
|
return true;
|
|
499
539
|
}
|
|
500
540
|
|
|
@@ -603,7 +643,11 @@ export function createTurnView({
|
|
|
603
643
|
if (blockEl) {
|
|
604
644
|
updateBlockSummary();
|
|
605
645
|
blockEl.classList.add("done");
|
|
606
|
-
|
|
646
|
+
// The outline (turn_model.js outlineRuns): open on its narrated steps'
|
|
647
|
+
// rows, as a reload draws it; a block with nothing said closes.
|
|
648
|
+
if (newestFirst) chronological();
|
|
649
|
+
const outlined = outlineBlock(blockEl, model.blockGens(turn));
|
|
650
|
+
if (!blockToggled) blockEl.open = outlined;
|
|
607
651
|
for (const el of blockEl.querySelectorAll(".plugin-card")) {
|
|
608
652
|
if (leavesBlock({ warn: el.classList.contains("warn"), kind })) kept.push(el);
|
|
609
653
|
}
|
|
@@ -615,6 +659,20 @@ export function createTurnView({
|
|
|
615
659
|
return kept;
|
|
616
660
|
}
|
|
617
661
|
|
|
662
|
+
// The steps back in chronological order after newestFirst (a no-op
|
|
663
|
+
// otherwise); a fold (outlineBlock) stays before its run's first step.
|
|
664
|
+
function chronological() {
|
|
665
|
+
if (!blockEl || !turn) return;
|
|
666
|
+
for (const gen of turn.gens) {
|
|
667
|
+
const entry = genEls.get(gen);
|
|
668
|
+
if (entry?.el.parentNode !== blockEl) continue;
|
|
669
|
+
const nodes = stepNodes(entry);
|
|
670
|
+
const fold = nodes[0].previousSibling;
|
|
671
|
+
if (fold?.classList?.contains("outline-fold")) nodes.unshift(fold);
|
|
672
|
+
blockEl.append(...nodes);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
618
676
|
function reset() {
|
|
619
677
|
hold.finish();
|
|
620
678
|
turn = null;
|
|
@@ -712,17 +770,14 @@ export function createTurnView({
|
|
|
712
770
|
// block, a step's elements by index, and the steps back in
|
|
713
771
|
// chronological order after newestFirst.
|
|
714
772
|
current() { return { turn, blockEl }; },
|
|
773
|
+
// The running turn's asking reply (turn_model.js askingText): what a
|
|
774
|
+
// question or approval card keeps once answered.
|
|
775
|
+
askingText: () => model.askingText(turn),
|
|
715
776
|
step(i) {
|
|
716
777
|
const entry = turn && genEls.get(turn.gens[i]);
|
|
717
778
|
return entry ? { el: entry.el, summary: entry.el.querySelector(":scope > summary"), thinkingWrap: thinkingWrap(entry) } : null;
|
|
718
779
|
},
|
|
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
|
-
},
|
|
780
|
+
chronological,
|
|
726
781
|
};
|
|
727
782
|
}
|
|
728
783
|
|
|
@@ -737,29 +792,23 @@ function defaultPlaceAnswer(appendToHistory) {
|
|
|
737
792
|
|
|
738
793
|
// ---- A reload ----
|
|
739
794
|
|
|
795
|
+
// The text of a reloaded block's step +iteration+ (1-based, as the snapshot's
|
|
796
|
+
// cards count), "" when it has none: a reloaded question card's asking
|
|
797
|
+
// reply.
|
|
798
|
+
export function stepText(block, iteration) {
|
|
799
|
+
const step = block?.querySelectorAll(":scope > .gen")[Number(iteration) - 1];
|
|
800
|
+
return step?.querySelector(":scope > .gen-text")?.textContent || "";
|
|
801
|
+
}
|
|
802
|
+
|
|
740
803
|
// A re-rendered failed turn's line (a .bubble.cancel, as live's
|
|
741
804
|
// addEndBubble), in turn_failed's words (failedTurnText) from what the turn
|
|
742
805
|
// record kept of it (SessionMetrics' failure: summary, message,
|
|
743
|
-
// error_class, kept_steps);
|
|
744
|
-
// before records kept that.
|
|
745
|
-
function
|
|
806
|
+
// error_class, kept_steps); nothing for any other record, or a failed one
|
|
807
|
+
// saved before records kept that.
|
|
808
|
+
function failedLineTemplate(record) {
|
|
746
809
|
const failure = record?.status === "failed" ? record.failure : null;
|
|
747
|
-
if (!failure || typeof failure !== "object") return
|
|
748
|
-
return `<div class="bubble cancel">${
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// Parts (HTML strings and templates, in order) as one fragment.
|
|
752
|
-
function fragmentOf(parts) {
|
|
753
|
-
const out = document.createDocumentFragment();
|
|
754
|
-
let markup = "";
|
|
755
|
-
for (const part of parts) {
|
|
756
|
-
if (typeof part === "string") { markup += part; continue; }
|
|
757
|
-
if (markup) out.append(rawNodes(markup));
|
|
758
|
-
markup = "";
|
|
759
|
-
out.append(nodesOf(part));
|
|
760
|
-
}
|
|
761
|
-
if (markup) out.append(rawNodes(markup));
|
|
762
|
-
return out;
|
|
810
|
+
if (!failure || typeof failure !== "object") return nothing;
|
|
811
|
+
return html`<div class="bubble cancel">${failedTurnText(failure)}</div>`;
|
|
763
812
|
}
|
|
764
813
|
|
|
765
814
|
// A reloaded history in the turn view (renderHistory's object path), as
|
|
@@ -768,51 +817,51 @@ function fragmentOf(parts) {
|
|
|
768
817
|
// the thinking, params and output, from the saved messages; status and
|
|
769
818
|
// duration from the timing's tool records), then the answer and the turn's
|
|
770
819
|
// timing line under it.
|
|
771
|
-
// +thumbs(images)+ draws a prompt's or a tool row's image thumbs
|
|
820
|
+
// +thumbs(images)+ draws a prompt's or a tool row's image thumbs (a
|
|
821
|
+
// template, images.js thumbsTemplate).
|
|
772
822
|
export function turnHistoryNodes(items, timing, { thumbs }) {
|
|
773
|
-
const
|
|
823
|
+
const timingTemplate = (group) => {
|
|
774
824
|
const record = group.record;
|
|
775
825
|
// Numbered by its place in the turn records, as live (a failed turn
|
|
776
826
|
// whose prompt went back still counts).
|
|
777
827
|
const at = (timing?.turnRecords || []).indexOf(record);
|
|
778
828
|
return record && formatDuration(record.duration_ms)
|
|
779
|
-
? `<div class="turn-timing">${
|
|
780
|
-
:
|
|
829
|
+
? html`<div class="turn-timing">${turnTimingText(at >= 0 ? at + 1 : group.turnIndex + 1, record.duration_ms, { canceled: record.status === "canceled" })}</div>`
|
|
830
|
+
: nothing;
|
|
781
831
|
};
|
|
782
|
-
//
|
|
783
|
-
// leaves it at the turn's end. +at+: the message's index, for its refs
|
|
832
|
+
// An answer's bubble; +at+, the message's index, is for its refs
|
|
784
833
|
// (data-answer-at, gone before the nodes are returned).
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
};
|
|
834
|
+
const answerBubble = (at, cls = "") => answerBubbleTemplate(items[at], { cls, at });
|
|
835
|
+
// The timing line after the bubble, not in it: where the live view
|
|
836
|
+
// leaves it at the turn's end.
|
|
837
|
+
const answerTemplate = (at, timing) => html`${answerBubble(at)}${timing}`;
|
|
790
838
|
// A user's line, labelled when chi brought it in (a delegate report). A
|
|
791
839
|
// `!cmd`'s saved message (the worker's "!(<command>)\n<output>") renders
|
|
792
840
|
// as its command bubble, as live does — not as a user bubble.
|
|
793
|
-
const
|
|
841
|
+
const userTemplate = (m, opts = {}) => {
|
|
794
842
|
const shell = shellCommandMessage(m?.content);
|
|
795
|
-
if (shell) return
|
|
796
|
-
return
|
|
843
|
+
if (shell) return commandBubbleTemplate(shell);
|
|
844
|
+
return userBubbleTemplate(m, { label: sourceLabel(m?.source), thumbs: thumbs(m.images), ...opts });
|
|
797
845
|
};
|
|
798
846
|
// The first text after a delegate report merged mid-turn, unless it is
|
|
799
847
|
// the turn's answer (shown anyway).
|
|
800
|
-
const
|
|
801
|
-
if (items[i]?.source !== "delegate_report") return
|
|
848
|
+
const reportReplyTemplate = (i, group) => {
|
|
849
|
+
if (items[i]?.source !== "delegate_report") return nothing;
|
|
802
850
|
const j = items.findIndex((m, k) => k > i && m?.role === "assistant" && String(m.content ?? "").trim());
|
|
803
|
-
if (j < 0 || j === group.answer || items.slice(i + 1, j).some((m) => m?.role === "user" && !m.merged)) return
|
|
804
|
-
|
|
805
|
-
return `<div class="bubble output report-reply${renderedMarkdown ? " markdown" : ""}" data-answer-at="${j}"${copySourceAttr(items[j], renderedMarkdown)}>${body}</div>`;
|
|
851
|
+
if (j < 0 || j === group.answer || items.slice(i + 1, j).some((m) => m?.role === "user" && !m.merged)) return nothing;
|
|
852
|
+
return answerBubble(j, " report-reply");
|
|
806
853
|
};
|
|
807
|
-
const
|
|
854
|
+
const steerTemplate = (i, bubble) => steerRowTemplate({ source: items[i].source, text: items[i].content }, { bubble });
|
|
808
855
|
const parts = [];
|
|
856
|
+
// Each block's steps, in order: its outline once the nodes exist.
|
|
857
|
+
const outlines = [];
|
|
809
858
|
for (const group of turnGroups(items, timing)) {
|
|
810
|
-
if (group.kind === "note") { parts.push(
|
|
811
|
-
if (group.kind === "steer") { parts.push(
|
|
812
|
-
if (group.kind === "answer") { parts.push(
|
|
859
|
+
if (group.kind === "note") { parts.push(noteBubbleTemplate(items[group.i])); continue; }
|
|
860
|
+
if (group.kind === "steer") { parts.push(steerTemplate(group.i, true)); continue; }
|
|
861
|
+
if (group.kind === "answer") { parts.push(answerTemplate(group.answer, nothing)); continue; }
|
|
813
862
|
const prompt = items[group.user];
|
|
814
863
|
// A context wake turn starts with its note.
|
|
815
|
-
parts.push(prompt.role === "note" ?
|
|
864
|
+
parts.push(prompt.role === "note" ? noteBubbleTemplate(prompt) : userTemplate(prompt));
|
|
816
865
|
const [only] = group.steps;
|
|
817
866
|
const ended = group.answer !== null || group.emptyAnswer != null;
|
|
818
867
|
const textless = only && (only.i === null || !String(items[only.i].content ?? ""));
|
|
@@ -826,36 +875,38 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
|
|
|
826
875
|
const gens = group.steps.map((step) => ({ text: step.i === null ? "" : String(items[step.i].content ?? ""), thinking: step.thinking || "", tools: step.tools }));
|
|
827
876
|
const steps = gens.map((gen, k) => stepTemplate(gen, {
|
|
828
877
|
// The steers the step answered first, where the live rows went.
|
|
829
|
-
steers: (group.steps[k].steers || []).map((i) =>
|
|
830
|
-
items: gen.tools.length ? [
|
|
878
|
+
steers: (group.steps[k].steers || []).map((i) => steerTemplate(i, false)),
|
|
879
|
+
items: gen.tools.length ? [rowNodes(gen.tools, thumbs)] : [],
|
|
831
880
|
}));
|
|
832
881
|
parts.push(html`<details class="turn-work done"><summary>${model.blockSummary({ gens, ended: true })}</summary>${steps}</details>`);
|
|
882
|
+
outlines.push(gens);
|
|
833
883
|
}
|
|
834
884
|
// Steers with no step to sit in, or in a turn that shows no block: on
|
|
835
885
|
// their own after the prompt.
|
|
836
886
|
const loose = noBlock ? [...group.steps.flatMap((step) => step.steers || []), ...(group.steers || [])] : group.steers || [];
|
|
837
|
-
parts.push(loose.map((i) =>
|
|
887
|
+
parts.push(loose.map((i) => steerTemplate(i, true)));
|
|
838
888
|
// The user's lines merged into the turn: the live "steered" bubbles,
|
|
839
889
|
// after its steps and before its answer. A delegate report's is
|
|
840
890
|
// followed by the text that answered it when that isn't the answer: a
|
|
841
891
|
// small model tells its user about a report before its next tool call,
|
|
842
892
|
// and that step's text would stay folded in the block.
|
|
843
893
|
parts.push((group.merged || []).map((i) =>
|
|
844
|
-
|
|
845
|
-
reportReplyHtml(i, group)).join(""));
|
|
894
|
+
html`${userTemplate(items[i], { state: "steered", step: items[i].step })}${reportReplyTemplate(i, group)}`));
|
|
846
895
|
// A turn that ended with no answer: chi's muted notice where the answer
|
|
847
896
|
// would be, before the timing, as live.
|
|
848
|
-
if (group.emptyAnswer != null) parts.push(`${
|
|
897
|
+
if (group.emptyAnswer != null) parts.push(html`${emptyAnswerTemplate(items[group.emptyAnswer])}${timingTemplate(group)}`);
|
|
849
898
|
// A turn with no answer (canceled, or running: no record yet) shows its
|
|
850
899
|
// timing after its prompt and steps, where the live view leaves it.
|
|
851
|
-
else if (group.answer === null) parts.push(
|
|
852
|
-
else parts.push(
|
|
900
|
+
else if (group.answer === null) parts.push(timingTemplate(group));
|
|
901
|
+
else parts.push(answerTemplate(group.answer, timingTemplate(group)));
|
|
853
902
|
// A canceled turn ends with its cancel line, a failed one whose steps
|
|
854
903
|
// stayed with its failure line, as the live view left them.
|
|
855
|
-
parts.push(
|
|
856
|
-
parts.push(
|
|
904
|
+
parts.push(cancelLineTemplate(group.record));
|
|
905
|
+
parts.push(failedLineTemplate(group.record));
|
|
857
906
|
}
|
|
858
|
-
const out =
|
|
907
|
+
const out = nodesOf(parts);
|
|
908
|
+
// A finished turn's block as the live end leaves it: its outline, open.
|
|
909
|
+
[...out.children].filter((el) => el.matches(".turn-work")).forEach((block, i) => outlineBlock(block, outlines[i] || []));
|
|
859
910
|
// Each answer bubble knows its message's refs (answer_refs.js; the
|
|
860
911
|
// page pairs them after the render), the index stays out of the DOM.
|
|
861
912
|
for (const bubble of out.querySelectorAll("[data-answer-at]")) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// lit-html 3.3.3 (npm tarball lit-html-3.3.3.tgz, directive.js: the minified production
|
|
2
|
+
// build, unmodified but for this line and the dropped sourceMappingURL). BSD-3-Clause.
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright 2017 Google LLC
|
|
6
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
7
|
+
*/
|
|
8
|
+
const t={ATTRIBUTE:1,CHILD:2,PROPERTY:3,BOOLEAN_ATTRIBUTE:4,EVENT:5,ELEMENT:6},e=t=>(...e)=>({_$litDirective$:t,values:e});class i{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}export{i as Directive,t as PartType,e as directive};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// lit-html 3.3.3 (npm tarball lit-html-3.3.3.tgz, directives/unsafe-html.js: the minified
|
|
2
|
+
// production build, unmodified but for this line and the dropped sourceMappingURL). BSD-3-Clause.
|
|
3
|
+
import{nothing as t,noChange as i}from"../lit-html.js";import{directive as r,Directive as s,PartType as n}from"../directive.js";
|
|
4
|
+
/**
|
|
5
|
+
* @license
|
|
6
|
+
* Copyright 2017 Google LLC
|
|
7
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
8
|
+
*/class e extends s{constructor(i){if(super(i),this.it=t,i.type!==n.CHILD)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(r){if(r===t||null==r)return this._t=void 0,this.it=r;if(r===i)return r;if("string"!=typeof r)throw Error(this.constructor.directiveName+"() called with a non-string value");if(r===this.it)return this._t;this.it=r;const s=[r];return s.raw=s,this._t={_$litType$:this.constructor.resultType,strings:s,values:[]}}}e.directiveName="unsafeHTML",e.resultType=1;const o=r(e);export{e as UnsafeHTMLDirective,o as unsafeHTML};
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -468,10 +468,10 @@ module Samagotchi
|
|
|
468
468
|
# would keep the hub's "needs you" badge on for good. Dropped and saved
|
|
469
469
|
# before the Engine and the Bridge exist (no question lock, no events).
|
|
470
470
|
def drop_dead_question
|
|
471
|
-
|
|
471
|
+
id = @session.drop_dead_question
|
|
472
|
+
return unless id
|
|
472
473
|
|
|
473
|
-
Log.info(:worker, "dead_question_dropped", id:
|
|
474
|
-
@session.pending_question = nil
|
|
474
|
+
Log.info(:worker, "dead_question_dropped", id: id)
|
|
475
475
|
save_or_log(:dead_question) { @session.save(state_dir: @state_dir) }
|
|
476
476
|
end
|
|
477
477
|
|
|
@@ -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
|
|
@@ -1102,22 +1103,36 @@ module Samagotchi
|
|
|
1102
1103
|
end
|
|
1103
1104
|
|
|
1104
1105
|
# Check again with the event log held, which the Bridge holds while it
|
|
1105
|
-
# queues a POST /turn, then close the Bridge so no client can
|
|
1106
|
-
# after the check, and stop the idle jobs (reminder callback,
|
|
1106
|
+
# queues a POST /turn, then close the Bridge's intake so no client can
|
|
1107
|
+
# queue one after the check, and stop the idle jobs (reminder callback,
|
|
1108
|
+
# recap). The Bridge stops after the log is let go (#stop_bridge_after).
|
|
1107
1109
|
# A client connecting from here on finds no worker: `chi --attach` fails
|
|
1108
1110
|
# and the web stream answers 503 (a small window, left as is).
|
|
1109
1111
|
# @return [Boolean] false when something came in since #due?
|
|
1110
1112
|
def leave_idle
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1113
|
+
stop_bridge_after do
|
|
1114
|
+
@engine.synchronize_events do
|
|
1115
|
+
next false unless @idle_exit.due?
|
|
1116
|
+
|
|
1117
|
+
@bridge&.close_intake
|
|
1118
|
+
@engine.stop_idle
|
|
1119
|
+
log_idle_exit
|
|
1120
|
+
true
|
|
1121
|
+
end
|
|
1118
1122
|
end
|
|
1119
1123
|
end
|
|
1120
1124
|
|
|
1125
|
+
# The Bridge stops once the block (which closed its intake with the
|
|
1126
|
+
# event log held) answers true, outside the log: Bridge#stop waits for
|
|
1127
|
+
# the requests it is answering, and one waiting on the log would hold
|
|
1128
|
+
# it for the whole grace before being killed.
|
|
1129
|
+
# @return [Boolean] what the block answered
|
|
1130
|
+
def stop_bridge_after
|
|
1131
|
+
leaving = yield
|
|
1132
|
+
@bridge&.stop if leaving
|
|
1133
|
+
leaving
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1121
1136
|
# Bridge POST /exit, on a Bridge thread with the event log held.
|
|
1122
1137
|
# +delete+: the session is deleted after (/exit --delete), so no recap.
|
|
1123
1138
|
# +restart+: a new worker takes over (on the newest chi installed), so
|
|
@@ -1139,29 +1154,31 @@ module Samagotchi
|
|
|
1139
1154
|
end
|
|
1140
1155
|
|
|
1141
1156
|
# #leave_idle for an exit a client asked for: check again with the event
|
|
1142
|
-
# log held (a prompt may have come in since), then close the Bridge
|
|
1143
|
-
# stop the idle jobs. Other clients' streams no longer hold: the asker's
|
|
1157
|
+
# log held (a prompt may have come in since), then close the Bridge's
|
|
1158
|
+
# intake and stop the idle jobs (the Bridge stops after the log). Other clients' streams no longer hold: the asker's
|
|
1144
1159
|
# may not have closed yet, and a UI joining now finds the worker gone
|
|
1145
1160
|
# (:stream_closed), the window #leave_idle has too.
|
|
1146
1161
|
# @return [Boolean] false when something came in since the request
|
|
1147
1162
|
def leave_on_request
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1163
|
+
stop_bridge_after do
|
|
1164
|
+
@engine.synchronize_events do
|
|
1165
|
+
hold = if @exit_restart
|
|
1166
|
+
@idle_exit.hold_for_restart
|
|
1167
|
+
else
|
|
1168
|
+
@idle_exit.hold_for_request(requester: @exit_requested_by, streams: false)
|
|
1169
|
+
end
|
|
1170
|
+
if hold
|
|
1171
|
+
@exit_requested = nil
|
|
1172
|
+
@exit_restart = false
|
|
1173
|
+
Log.info(:worker, "exit_held", reason: hold)
|
|
1174
|
+
next false
|
|
1175
|
+
end
|
|
1160
1176
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1177
|
+
@bridge&.close_intake
|
|
1178
|
+
@engine.stop_idle
|
|
1179
|
+
Log.info(:worker, @exit_restart ? "restart_requested" : "exit_requested", by: @exit_requested_by || "a client")
|
|
1180
|
+
true
|
|
1181
|
+
end
|
|
1165
1182
|
end
|
|
1166
1183
|
end
|
|
1167
1184
|
|
|
@@ -106,9 +106,10 @@ module Samagotchi
|
|
|
106
106
|
# run): auto-attached context alone doesn't make a session worth
|
|
107
107
|
# keeping. Saved, then the subscriptions written (a crash between the
|
|
108
108
|
# two re-delivers; the note ids dedupe).
|
|
109
|
-
# +wake+: the session is idle with nothing queued, so
|
|
110
|
-
# source asked to wake
|
|
111
|
-
#
|
|
109
|
+
# +wake+: the session is idle with nothing queued, so a delivery with a
|
|
110
|
+
# wake note (an update whose source asked to wake, or a session push
|
|
111
|
+
# source's first text that asked) may start a turn (#context_wake_for):
|
|
112
|
+
# its wake note goes in as the turn's first message.
|
|
112
113
|
# @return [ContextWake, nil] the wake turn to run now
|
|
113
114
|
# @raise [SystemCallError, IOError] the caller logs it (context_failed)
|
|
114
115
|
def absorb_context(before_turn: false, wake: false)
|
|
@@ -134,7 +135,7 @@ module Samagotchi
|
|
|
134
135
|
waking && ContextWake.new(delivery: waking, turn_id: turn_id)
|
|
135
136
|
end
|
|
136
137
|
|
|
137
|
-
# A failed wake turn kept nothing but its note: the
|
|
138
|
+
# A failed wake turn kept nothing but its note: the note goes back to
|
|
138
139
|
# a plain note (no turn start, the background wording), so a reload
|
|
139
140
|
# draws no empty turn for it.
|
|
140
141
|
def unmark_wake(context_wake)
|
|
@@ -201,9 +202,11 @@ module Samagotchi
|
|
|
201
202
|
|
|
202
203
|
private
|
|
203
204
|
|
|
204
|
-
# +unless_stopped+:
|
|
205
|
-
#
|
|
206
|
-
#
|
|
205
|
+
# +unless_stopped+: skipped when the session was stopped meanwhile (chi
|
|
206
|
+
# sessions stop, from another process: its marker file,
|
|
207
|
+
# Session.mark_stopped, which a load reads over the saved status). The
|
|
208
|
+
# save couldn't hide the stop; it is left out so the file stays as the
|
|
209
|
+
# stop found it, the worker's idle status unwritten.
|
|
207
210
|
def save(at, unless_stopped: false)
|
|
208
211
|
self.class.save_or_log(at) do
|
|
209
212
|
@session.save(state_dir: @state_dir) unless unless_stopped && @stopped.call
|
|
@@ -222,13 +225,13 @@ module Samagotchi
|
|
|
222
225
|
true
|
|
223
226
|
end
|
|
224
227
|
|
|
225
|
-
# The
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
# session.max_wakes, past the start
|
|
229
|
-
# the session in the last
|
|
230
|
-
#
|
|
231
|
-
# session was away.
|
|
228
|
+
# The delivery in +batch+ that may start a wake turn now: it has a wake
|
|
229
|
+
# note (ContextAbsorber decides: its source asked, wake: true),
|
|
230
|
+
# context.wake is on, the wake budget is open (WorkerWakes#context_open?:
|
|
231
|
+
# no continue offer, not paused, under session.max_wakes, past the start
|
|
232
|
+
# grace) and the source hasn't woken the session in the last
|
|
233
|
+
# CONTEXT_WAKE_WINDOW. The others stay plain notes. Within the start
|
|
234
|
+
# grace a change is a note: it came while the session was away.
|
|
232
235
|
def context_wake_for(batch, now: Time.now)
|
|
233
236
|
candidates = batch.deliveries.select(&:wake_note)
|
|
234
237
|
return nil if candidates.empty? || !context_wakes_on?
|