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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -1
- data/bin/chi +3 -2
- data/docs/architecture.md +6 -2
- data/docs/cli.md +29 -7
- data/docs/configuration.md +113 -8
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/context-telemetry.md +23 -5
- data/docs/internals/engine.md +54 -18
- data/docs/internals/prompt-caching.md +8 -0
- data/docs/sessions.md +2 -2
- data/docs/testing.md +23 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +6 -6
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cache_confirm.rb +51 -0
- data/lib/samagotchi/client_id.rb +12 -0
- data/lib/samagotchi/config.rb +9 -3
- data/lib/samagotchi/engine.rb +102 -8
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/file_ref.rb +59 -0
- data/lib/samagotchi/host_config.rb +6 -3
- data/lib/samagotchi/host_registry.rb +4 -3
- data/lib/samagotchi/kernel_loop.rb +99 -16
- data/lib/samagotchi/llm/chat_loop.rb +122 -71
- data/lib/samagotchi/llm/openai_chat.rb +2 -1
- data/lib/samagotchi/llm/stream_drops.rb +63 -0
- data/lib/samagotchi/llm/turn_settings.rb +9 -5
- data/lib/samagotchi/model_settings.rb +5 -2
- data/lib/samagotchi/reasoning_history.rb +132 -0
- data/lib/samagotchi/reasoning_history_command.rb +107 -0
- data/lib/samagotchi/self_report.rb +21 -0
- data/lib/samagotchi/session.rb +23 -2
- data/lib/samagotchi/session_commands.rb +55 -15
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_setup.rb +7 -5
- data/lib/samagotchi/setting_explained.rb +20 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +2 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +11 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +8 -5
- data/lib/samagotchi/terminal_ui.rb +13 -6
- data/lib/samagotchi/thinking.rb +20 -25
- data/lib/samagotchi/thinking_command.rb +33 -11
- data/lib/samagotchi/tool_row_fields.rb +46 -0
- data/lib/samagotchi/tool_runner.rb +7 -11
- data/lib/samagotchi/tools/builtins.rb +6 -2
- data/lib/samagotchi/tools/peers.rb +4 -2
- data/lib/samagotchi/tools/task_runtime.rb +2 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +48 -4
- data/lib/samagotchi/web/capabilities.rb +13 -9
- data/lib/samagotchi/web/editor.rb +38 -0
- data/lib/samagotchi/web/message_parts.rb +5 -10
- data/lib/samagotchi/web/public/activity.js +33 -18
- data/lib/samagotchi/web/public/app.js +256 -1133
- data/lib/samagotchi/web/public/commands.js +269 -0
- data/lib/samagotchi/web/public/composer.js +105 -0
- data/lib/samagotchi/web/public/diff_view.js +30 -4
- data/lib/samagotchi/web/public/index.html +11 -3
- data/lib/samagotchi/web/public/info_bar.js +80 -0
- data/lib/samagotchi/web/public/level_chip.js +174 -0
- data/lib/samagotchi/web/public/lists_view.js +725 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +9 -6
- data/lib/samagotchi/web/public/model_pick.js +1 -1
- data/lib/samagotchi/web/public/question_cards.js +2 -1
- data/lib/samagotchi/web/public/reasoning_history_chip.js +49 -0
- data/lib/samagotchi/web/public/refs.js +134 -0
- data/lib/samagotchi/web/public/restore.js +81 -0
- data/lib/samagotchi/web/public/session_state.js +57 -0
- data/lib/samagotchi/web/public/stream_handlers.js +332 -0
- data/lib/samagotchi/web/public/thinking_chip.js +38 -144
- data/lib/samagotchi/web/public/turn_events.js +17 -258
- data/lib/samagotchi/web/public/turn_html.js +5 -1
- data/lib/samagotchi/web/server.rb +4 -4
- data/lib/samagotchi/worker.rb +17 -5
- metadata +18 -2
- data/lib/samagotchi/web/public/stable_html.js +0 -18
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Pure helpers for the Bridge's turn events (no DOM), so app.js's handlers
|
|
2
2
|
// stay thin and the logic runs under `node --test`.
|
|
3
3
|
|
|
4
|
+
import { ACTIVITY_FIELDS, EVENT_FIELDS, pickFields } from "./activity.js";
|
|
5
|
+
|
|
4
6
|
// The answer text of a :turn_completed event. `turn_summary.output` is the
|
|
5
7
|
// contract; `result` is the KernelLoop result, which reaches the wire as its
|
|
6
8
|
// string form (older servers, other clients' fixtures).
|
|
@@ -189,6 +191,11 @@ export function llmContextHover({ groups = [], freed_tokens: freed, tail_tokens:
|
|
|
189
191
|
return lines.join("\n");
|
|
190
192
|
}
|
|
191
193
|
|
|
194
|
+
// A ✂ row's notice data (turnDom.notice, placeTurnNotice).
|
|
195
|
+
export function llmContextNotice(data) {
|
|
196
|
+
return { line: llmContextLine(data), cls: "llm-context", title: llmContextHover(data) };
|
|
197
|
+
}
|
|
198
|
+
|
|
192
199
|
// The one muted line for a turn that ended with no answer (turn_completed's
|
|
193
200
|
// turn_summary.empty_answer, or a reloaded turn's marker): the TUI words
|
|
194
201
|
// it the same (TurnNote.empty_answer_line; spec/shared/labels_matrix.json).
|
|
@@ -229,257 +236,6 @@ export function initWaitLine({ tasks } = {}) {
|
|
|
229
236
|
return `waiting for ${labels.length ? labels.join(" · ") : "plugins"}…`;
|
|
230
237
|
}
|
|
231
238
|
|
|
232
|
-
// Whether a composer line in an open session is one of its commands
|
|
233
|
-
// (+commands+: the session GET's listing), as the terminals decide
|
|
234
|
-
// (Commands::Registry#lookup): a "!" line with something after the "!", or
|
|
235
|
-
// a listed name alone or followed by a space; a terminal's own (/stats,
|
|
236
|
-
// /exit) too, which the page answers itself (webLocalReply). An unknown
|
|
237
|
-
// "/word" (a path, a typo) goes to the model as a prompt, as in a terminal.
|
|
238
|
-
export function sessionCommandLine(text, commands) {
|
|
239
|
-
const line = String(text || "").trim();
|
|
240
|
-
if (/^!\s*\S/.test(line)) return true;
|
|
241
|
-
if (!line.startsWith("/")) return false;
|
|
242
|
-
if (webLocalReply(line)) return true;
|
|
243
|
-
return (Array.isArray(commands) ? commands : [])
|
|
244
|
-
.some((c) => c && typeof c.name === "string" && (line === c.name || line.startsWith(`${c.name} `)));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// /cut TEXT and /queue TEXT: the UIs' own words for a delivery (listed
|
|
248
|
-
// for Tab and /help, never run as commands; TerminalUI::Formatting's
|
|
249
|
-
// STEER_CHOICE: the word, then any whitespace and the text). During a
|
|
250
|
-
// turn /cut TEXT is the text sent as a cut (POST /turn, delivery "cut"),
|
|
251
|
-
// as chi send --cut does; /queue isn't in the web yet (the page says so,
|
|
252
|
-
// as the attached TUI does). With no running turn both are the text as a
|
|
253
|
-
// normal turn. The bare word gets the usage line.
|
|
254
|
-
// @return {{send: string, delivery?: string} | {reply: string} | null}
|
|
255
|
-
// null: not a delivery line
|
|
256
|
-
export const STEER_CHOICE_USAGE = "(/cut and /queue need a message: /cut TEXT, /queue TEXT)";
|
|
257
|
-
export const QUEUE_UNAVAILABLE_WEB = "(/queue isn't available in the web yet; a plain message goes in at the running turn's next step, and /cut TEXT cuts in)";
|
|
258
|
-
const STEER_CHOICE = /^\/(cut|queue)(?:\s+([\s\S]*))?$/;
|
|
259
|
-
|
|
260
|
-
export function steerChoiceRoute(text, { running = false } = {}) {
|
|
261
|
-
const match = STEER_CHOICE.exec(String(text || "").trim());
|
|
262
|
-
if (!match) return null;
|
|
263
|
-
const message = (match[2] || "").trim();
|
|
264
|
-
if (!message) return { reply: STEER_CHOICE_USAGE };
|
|
265
|
-
if (!running) return { send: message };
|
|
266
|
-
if (match[1] === "queue") return { reply: QUEUE_UNAVAILABLE_WEB };
|
|
267
|
-
return { send: message, delivery: "cut" };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// A line that is one word: a slash and a name, no spaces, no second slash
|
|
271
|
-
// (Commands::Registry#unknown_command_word?). `/modle` is one; `/foo bar`
|
|
272
|
-
// and `/usr/bin/env` are not (they are prompts).
|
|
273
|
-
const UNKNOWN_COMMAND_WORD = /^\/[A-Za-z][A-Za-z0-9_-]*$/;
|
|
274
|
-
|
|
275
|
-
// The hint for a command word no command answers (a typo like `/modle`),
|
|
276
|
-
// or null when the line is not one: `Unknown command /modle. Did you mean
|
|
277
|
-
// /model? /help lists the commands.` The "Did you mean" part needs a close
|
|
278
|
-
// name among the session's commands and the page's own (webLocalReply).
|
|
279
|
-
export function unknownCommandHint(text, commands) {
|
|
280
|
-
const line = String(text || "").trim();
|
|
281
|
-
if (!UNKNOWN_COMMAND_WORD.test(line)) return null;
|
|
282
|
-
if (sessionCommandLine(line, commands)) return null;
|
|
283
|
-
const names = (Array.isArray(commands) ? commands : [])
|
|
284
|
-
.map((c) => (c && typeof c.name === "string" ? c.name : null))
|
|
285
|
-
.filter(Boolean)
|
|
286
|
-
.concat(Object.keys(WEB_LOCAL_REPLIES));
|
|
287
|
-
const close = closestName(line, names);
|
|
288
|
-
return `Unknown command ${line}. ${close ? `Did you mean ${close}? ` : ""}/help lists the commands.`;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// The closest name within an optimal string alignment distance of 2, or
|
|
292
|
-
// null. Ties go to the first in the list.
|
|
293
|
-
function closestName(line, names) {
|
|
294
|
-
let best = null;
|
|
295
|
-
let bestDistance = 3;
|
|
296
|
-
for (const name of names) {
|
|
297
|
-
const distance = osaDistance(line, name);
|
|
298
|
-
if (distance < bestDistance) {
|
|
299
|
-
best = name;
|
|
300
|
-
bestDistance = distance;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return best;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Optimal string alignment distance: Levenshtein, with an adjacent
|
|
307
|
-
// transposition counting as 1 (so `/modle` is 1 from `/model`, as Ruby's
|
|
308
|
-
// DidYouMean reads it).
|
|
309
|
-
function osaDistance(a, b) {
|
|
310
|
-
const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
311
|
-
for (let j = 0; j <= b.length; j += 1) rows[0][j] = j;
|
|
312
|
-
for (let i = 1; i <= a.length; i += 1) {
|
|
313
|
-
for (let j = 1; j <= b.length; j += 1) {
|
|
314
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
315
|
-
rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
|
|
316
|
-
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
317
|
-
rows[i][j] = Math.min(rows[i][j], rows[i - 2][j - 2] + 1);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return rows[a.length][b.length];
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// The terminal's own commands (a button in the web, or none yet): the
|
|
325
|
-
// reply the page shows itself (they never go to the worker, which doesn't
|
|
326
|
-
// know them), or null.
|
|
327
|
-
const WEB_LOCAL_REPLIES = {
|
|
328
|
-
"/archive": "/archive: use the archive button in the session bar",
|
|
329
|
-
"/detach": "/detach: a terminal's command; close the tab to leave, the worker keeps running",
|
|
330
|
-
"/exit": "/exit: close the tab to leave; stop in the session bar stops the worker",
|
|
331
|
-
"/quit": "/quit: close the tab to leave; stop in the session bar stops the worker",
|
|
332
|
-
"/recap": "/recap: not in the web yet; a recap shows here by itself when you come back to an idle session, and /recap in a terminal (chi --attach) makes one now",
|
|
333
|
-
"/stats": "/stats: not in the web yet; each turn shows its time, the session bar the context use and speed (hover ctx for tokens and cost), and /stats in a terminal (chi --attach) has the rest",
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
export function webLocalReply(text) {
|
|
337
|
-
const name = String(text || "").trim().split(/\s+/)[0].toLowerCase();
|
|
338
|
-
return WEB_LOCAL_REPLIES[name] || null;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// The start page's first message when the page answers it itself
|
|
342
|
-
// (webLocalReply: /stats, /exit …): shown there, with no session made just
|
|
343
|
-
// to hold the reply. With images it is a message, as in a session.
|
|
344
|
-
export function startPageReply(text, { images = 0 } = {}) {
|
|
345
|
-
return images ? null : webLocalReply(text);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// The start page's reply to a first message that needs no session at all
|
|
349
|
-
// (2.29): its own command's reply (startPageReply), or the hint for a
|
|
350
|
-
// command word no command answers (a typo like /modle) — the hint the page
|
|
351
|
-
// shows in a session too, as a command bubble. Decided before the session
|
|
352
|
-
// is created, so a first /stats or /modle leaves none behind. A null here
|
|
353
|
-
// means the message goes to a session (a prompt, or anything with images).
|
|
354
|
-
export function startPageReplyOrHint(text, { commands = [], images = 0 } = {}) {
|
|
355
|
-
if (images) return null;
|
|
356
|
-
return startPageReply(text) || unknownCommandHint(text, commands);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// The continue card's answers, as the terminal's continue prompt reads them.
|
|
360
|
-
export function continueLine(choice, reason = "") {
|
|
361
|
-
const why = String(reason || "").trim();
|
|
362
|
-
if (choice === "no" && why) return `/continue no, ${why}`;
|
|
363
|
-
return `/continue ${choice}`;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// How to show a command_ran: `label` names another client (null for this
|
|
367
|
-
// tab's own), `resync` says the conversation changed (!rollback, !cmd, a
|
|
368
|
-
// continue answered no) and must be read again. An anytime command's
|
|
369
|
-
// (`anytime`, D8) bubble is drawn at its command_queued, before the cards
|
|
370
|
-
// it shows; its command_ran fills that bubble in (`commandId`). So is a
|
|
371
|
-
// command queued for after the running turn (`queued`): its command_queued
|
|
372
|
-
// (waits: "turn_end") draws it `waiting`, saying so, until it ran or was
|
|
373
|
-
// dropped (styled as failed).
|
|
374
|
-
export const QUEUED_TEXT = "queued: runs after this turn";
|
|
375
|
-
|
|
376
|
-
export function commandView(event, myId) {
|
|
377
|
-
const own = isOwn(event.client_id, myId);
|
|
378
|
-
const waiting = event.waits === "turn_end";
|
|
379
|
-
return {
|
|
380
|
-
label: own ? null : clientLabel(event.client_id),
|
|
381
|
-
line: event.line || "",
|
|
382
|
-
text: event.output || (waiting ? QUEUED_TEXT : ""),
|
|
383
|
-
busy: event.status === "busy",
|
|
384
|
-
failed: event.status === "error" || event.status === "dropped",
|
|
385
|
-
resync: (event.changed || []).includes("messages"),
|
|
386
|
-
modelName: event.model_name || null,
|
|
387
|
-
anytime: event.anytime === true,
|
|
388
|
-
queued: waiting || event.queued === true,
|
|
389
|
-
waiting,
|
|
390
|
-
commandId: event.command_id || null,
|
|
391
|
-
// A card's action (Nudge…): no bubble unless it says something back.
|
|
392
|
-
hidden: event.card === true && !event.output && event.status !== "error",
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Whether a command_ran that re-read the conversation (`resync`) needs its
|
|
397
|
-
// own bubble: the redrawn history ends with a command bubble for its line
|
|
398
|
-
// (a `!cmd` appends its "!(<command>)\n<output>" message, which renders as
|
|
399
|
-
// one), so a second bubble would show the output twice. +historyBubbles+:
|
|
400
|
-
// the `.bubble.command` elements in document order; +line+: the event's
|
|
401
|
-
// line. Only the last one counts: it is the redrawn history's end.
|
|
402
|
-
export function historyHasCommandBubble(historyBubbles, line) {
|
|
403
|
-
const last = historyBubbles[historyBubbles.length - 1];
|
|
404
|
-
return !!last && last.querySelector(":scope > .command-line")?.textContent === line;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// What a prompt_restored (the worker rolled a failed turn back and handed its
|
|
408
|
-
// prompt back) does in this tab: `refill` is the text for an empty composer,
|
|
409
|
-
// only for a prompt this page sent (`sentIds`, its acks' enqueued_ids), so a
|
|
410
|
-
// replay after a reload never refills stale text; `own`/`label` mark the
|
|
411
|
-
// failed bubble shown again after the resync.
|
|
412
|
-
export function restoreAction(event, { myId, sentIds, composerEmpty }) {
|
|
413
|
-
const origin = event?.origin || {};
|
|
414
|
-
const own = isOwn(origin.client_id, myId);
|
|
415
|
-
const sent = own && !!origin.enqueued_id && sentIds.has(origin.enqueued_id);
|
|
416
|
-
// The images go back as chips with the text.
|
|
417
|
-
return withImages({
|
|
418
|
-
refill: sent && composerEmpty ? event.prompt : null,
|
|
419
|
-
own,
|
|
420
|
-
label: own ? null : clientLabel(origin.client_id),
|
|
421
|
-
}, sent && composerEmpty ? event.images : null);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// A turn that fails fast can have its prompt_restored come over the stream
|
|
425
|
-
// before the /turn ack names its enqueued_id: `early` (a Map) keeps such a
|
|
426
|
-
// restore of this tab's own prompt by that id, and the ack takes it back
|
|
427
|
-
// (restoreOnAck), so the prompt goes into the composer the ack would clear.
|
|
428
|
-
export function keepEarlyRestore(event, early, { myId, sentIds }) {
|
|
429
|
-
const origin = event?.origin || {};
|
|
430
|
-
const id = origin.enqueued_id;
|
|
431
|
-
if (!isOwn(origin.client_id, myId) || !id || sentIds.has(id)) return false;
|
|
432
|
-
early.set(id, event);
|
|
433
|
-
return true;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// The ack's enqueued_id (already in sentIds) was restored early: what the
|
|
437
|
-
// composer gets back (as restoreAction for an empty composer), else null.
|
|
438
|
-
export function restoreOnAck(enqueuedId, early, { myId, sentIds }) {
|
|
439
|
-
const event = enqueuedId && early.get(enqueuedId);
|
|
440
|
-
if (!event) return null;
|
|
441
|
-
early.delete(enqueuedId);
|
|
442
|
-
return restoreAction(event, { myId, sentIds, composerEmpty: true });
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// The /turn this kept entry was for failed: no ack comes, so nothing would
|
|
446
|
-
// take it back. The whole map goes: this tab sends one turn at a time, so
|
|
447
|
-
// every entry waits for an ack that is now never coming (an older one is
|
|
448
|
-
// left over from a send that failed the same way).
|
|
449
|
-
export function dropEarlyRestores(early) {
|
|
450
|
-
const count = early.size;
|
|
451
|
-
early.clear();
|
|
452
|
-
return count;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// The composer's text with a restored prompt put back into it: the restored
|
|
456
|
-
// text alone when nothing is typed, else the typed text with the restored
|
|
457
|
-
// prompt under it (the user typed between the restore and the ack; their
|
|
458
|
-
// text must not be replaced).
|
|
459
|
-
export function restoreInto(refill, current) {
|
|
460
|
-
const restored = String(refill ?? "");
|
|
461
|
-
const typed = String(current ?? "");
|
|
462
|
-
if (!restored) return typed;
|
|
463
|
-
if (!typed.trim()) return restored;
|
|
464
|
-
return `${typed.replace(/\n+$/, "")}\n${restored}`;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// What the composer holds once a /turn ack lands (4.13): the composer still
|
|
468
|
-
// carries the text that was just sent, so that text goes (it is the history's
|
|
469
|
-
// now) — but text typed since the send stays, and a prompt restored before
|
|
470
|
-
// this ack (restoreOnAck's refill) is put back without replacing it. The
|
|
471
|
-
// sent text is trimmed (turnText), so the composer's is too before they are
|
|
472
|
-
// compared: a trailing newline or space must not keep the message; and an
|
|
473
|
-
// image-only send (placeholders sent) leaves nothing worth keeping.
|
|
474
|
-
// @param sent the text the turn was sent with
|
|
475
|
-
// @param current the composer's text as the ack lands
|
|
476
|
-
// @param refill the prompt an early restore handed back, if any
|
|
477
|
-
export function composerAfterAck({ sent = "", current = "", refill = "" } = {}) {
|
|
478
|
-
const typed = String(current ?? "").trim();
|
|
479
|
-
const kept = !typed || typed === String(sent ?? "").trim() ? "" : current;
|
|
480
|
-
return restoreInto(refill, kept);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
239
|
// What an event does to the prompt bubbles, as a list of ops for app.js:
|
|
484
240
|
// add — a bubble for a prompt this tab hasn't shown (state queued /
|
|
485
241
|
// started / steered, labelled by its sender)
|
|
@@ -580,10 +336,11 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
|
|
|
580
336
|
closeText();
|
|
581
337
|
const call = { iteration: part.iteration, call_index: part.call_index, tool: part.tool };
|
|
582
338
|
if (part.label) call.label = part.label;
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
339
|
+
// Its row fields, each in its place (activity.js ACTIVITY_FIELDS,
|
|
340
|
+
// EVENT_FIELDS), as the live events carry them.
|
|
341
|
+
const title = pickFields(part, ACTIVITY_FIELDS);
|
|
342
|
+
const fields = pickFields(part, EVENT_FIELDS);
|
|
343
|
+
events.push({ type: "tool_call_started", ...call, params: part.params, ...title, ...fields });
|
|
587
344
|
if (part.status !== "running") {
|
|
588
345
|
// The activity as the live event had it; action and duration_ms
|
|
589
346
|
// only in snapshots from a worker that keeps them (the TUI's
|
|
@@ -591,11 +348,13 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
|
|
|
591
348
|
const action = part.action ? { action: part.action } : {};
|
|
592
349
|
const completed = {
|
|
593
350
|
type: "tool_call_completed", ...call, output: part.output, output_truncated: !!part.output_truncated,
|
|
594
|
-
activity: { ...action, tool: part.tool, status: part.status, params: part.params, ...title
|
|
351
|
+
activity: { ...action, tool: part.tool, status: part.status, params: part.params, ...title },
|
|
595
352
|
};
|
|
353
|
+
// A grep that found nothing: its row stays muted "no match".
|
|
354
|
+
if (part.no_match) completed.activity.no_match = true;
|
|
596
355
|
if (part.duration_ms != null) completed.duration_ms = part.duration_ms;
|
|
597
356
|
if (part.diff) completed.diff = part.diff;
|
|
598
|
-
|
|
357
|
+
Object.assign(completed, fields);
|
|
599
358
|
events.push(withImages(completed, part.images));
|
|
600
359
|
}
|
|
601
360
|
break;
|
|
@@ -637,6 +396,6 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
|
|
|
637
396
|
|
|
638
397
|
// +object+ with `images` when there are any (the shapes stay as they were
|
|
639
398
|
// for a turn without).
|
|
640
|
-
function withImages(object, images) {
|
|
399
|
+
export function withImages(object, images) {
|
|
641
400
|
return images?.length ? { ...object, images } : object;
|
|
642
401
|
}
|
|
@@ -7,6 +7,7 @@ import { commandBlockHtml, rowHover } from "./command_view.js";
|
|
|
7
7
|
import { copySourceAttr } from "./copy.js";
|
|
8
8
|
import { activityDiffHtml } from "./diff_view.js";
|
|
9
9
|
import { escapeHtml, userBodyHtml } from "./format.js";
|
|
10
|
+
import { refAttrs } from "./refs.js";
|
|
10
11
|
import { formatDuration } from "./timing.js";
|
|
11
12
|
import { emptyAnswerLine } from "./turn_events.js";
|
|
12
13
|
|
|
@@ -81,7 +82,10 @@ export function toolRowHtml(row, { thumbs = () => "" } = {}) {
|
|
|
81
82
|
export function toolRowInnerHtml(row, { thumbs = () => "" } = {}) {
|
|
82
83
|
const shown = row.title || row.params;
|
|
83
84
|
const hover = rowHover(row) ? ` title="${escapeHtml(rowHover(row))}"` : "";
|
|
84
|
-
|
|
85
|
+
// A row with a ref (a file tool's file) carries it on its title, which
|
|
86
|
+
// opens it, and its diff lines open it at their line (refs.js).
|
|
87
|
+
const ref = shown ? refAttrs(row.ref) : "";
|
|
88
|
+
const params = shown ? `<span class="activity-params${ref ? " ref" : ""}"${hover}${ref}>${escapeHtml(shown)}</span>` : "";
|
|
85
89
|
// The name the model called the tool by when it was an alias's, dim.
|
|
86
90
|
const alias = row.called_as ? `<span class="activity-alias">(called as ${escapeHtml(row.called_as)})</span>` : "";
|
|
87
91
|
const duration = formatDuration(row.duration_ms);
|
|
@@ -66,7 +66,7 @@ module Samagotchi
|
|
|
66
66
|
# @return [Integer] the exit status
|
|
67
67
|
def self.launch(port: nil, host: nil, scope: "project", dir: Dir.pwd, open_browser: false, markdown: false,
|
|
68
68
|
view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default,
|
|
69
|
-
new_token: false)
|
|
69
|
+
editor: Config::BY_KEY["web.editor"].default, new_token: false)
|
|
70
70
|
rotate_token if new_token
|
|
71
71
|
setting = host_setting(host)
|
|
72
72
|
lan = Lan.wanted?(setting) ? Lan.choose(setting) : nil
|
|
@@ -95,7 +95,7 @@ module Samagotchi
|
|
|
95
95
|
return 0
|
|
96
96
|
end
|
|
97
97
|
if start(port: port, host: host, url: url, open_browser: open_browser, markdown: markdown, view: view,
|
|
98
|
-
annotate_presets: annotate_presets, lan: lan)
|
|
98
|
+
annotate_presets: annotate_presets, editor: editor, lan: lan)
|
|
99
99
|
0
|
|
100
100
|
else
|
|
101
101
|
1
|
|
@@ -119,7 +119,7 @@ module Samagotchi
|
|
|
119
119
|
# @return [Boolean] false when the port was taken (said so on stderr)
|
|
120
120
|
def self.start(port: nil, host: nil, url: nil, open_browser: false, state_dir: nil, manager: nil, markdown: false,
|
|
121
121
|
view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default, hub: nil, lan: nil,
|
|
122
|
-
token_path: Token.path)
|
|
122
|
+
editor: Config::BY_KEY["web.editor"].default, token_path: Token.path)
|
|
123
123
|
port = resolve_port(port)
|
|
124
124
|
host = resolve_host(host)
|
|
125
125
|
url ||= "http://#{url_host(host)}:#{port}/"
|
|
@@ -130,7 +130,7 @@ module Samagotchi
|
|
|
130
130
|
lan_option = { ip: lan.ip, token: Token::Source.new(token_path) }
|
|
131
131
|
end
|
|
132
132
|
app = App.new(manager: manager, state_dir: state_dir, markdown: markdown, view: view,
|
|
133
|
-
annotate_presets: annotate_presets, hub: hub, lan: lan_option)
|
|
133
|
+
annotate_presets: annotate_presets, editor: editor, hub: hub, lan: lan_option)
|
|
134
134
|
Samagotchi::Log.info(:web, "start", url: "http://#{host}:#{port}", version: Samagotchi::VERSION)
|
|
135
135
|
Samagotchi::Log.info(:web, "lan", ip: lan.ip, interface: lan.interface) if lan
|
|
136
136
|
# Subscribed before the first scan, which makes the first check.
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -693,7 +693,8 @@ module Samagotchi
|
|
|
693
693
|
resolved = @continue_offer.resolved?(awaiting, result)
|
|
694
694
|
@engine.synchronize_events do
|
|
695
695
|
@continue_offer.announce_resolved(result, command) if resolved
|
|
696
|
-
announce_command(command, status: result.status.to_s, output: result.output, changed: Array(result.changed)
|
|
696
|
+
announce_command(command, status: result.status.to_s, output: result.output, changed: Array(result.changed),
|
|
697
|
+
confirm: result.confirm)
|
|
697
698
|
shown.each { |event| @engine.announce(event) }
|
|
698
699
|
end
|
|
699
700
|
save_or_log(:command) { save_session } unless Array(result.changed).empty?
|
|
@@ -706,9 +707,14 @@ module Samagotchi
|
|
|
706
707
|
# the cards and notices it showed, held: they follow its command_ran,
|
|
707
708
|
# as its output
|
|
708
709
|
def run_command_line(command)
|
|
709
|
-
@engine.holding_announcements { command_result(command) { @commands.run(command.line) } }
|
|
710
|
+
@engine.holding_announcements { command_result(command) { @commands.run(command.line, confirm: confirm?(command)) } }
|
|
710
711
|
end
|
|
711
712
|
|
|
713
|
+
# Whether +command+ asks before a costly /thinking or /reasoning-history
|
|
714
|
+
# change (CacheConfirm): only a person at a web tab or a TUI sees the
|
|
715
|
+
# question; chi send, a delegate, a plugin and a card's action apply it.
|
|
716
|
+
def confirm?(command) = ClientId.interactive?(command.client_id) && !command.card
|
|
717
|
+
|
|
712
718
|
# The block's Result for +command+; one that raised, or found no
|
|
713
719
|
# command for the line, is an error Result saying so.
|
|
714
720
|
# @return [SessionCommands::Result]
|
|
@@ -767,9 +773,9 @@ module Samagotchi
|
|
|
767
773
|
# saved.
|
|
768
774
|
def start_anytime_command(command)
|
|
769
775
|
@engine.spawn_anytime do
|
|
770
|
-
result = command_result(command) { @engine.running_anytime { @commands.run(command.line) } }
|
|
776
|
+
result = command_result(command) { @engine.running_anytime { @commands.run(command.line, confirm: confirm?(command)) } }
|
|
771
777
|
announce_command(command, status: result.status.to_s, output: result.output, changed: Array(result.changed),
|
|
772
|
-
anytime: true)
|
|
778
|
+
anytime: true, confirm: result.confirm)
|
|
773
779
|
rescue StandardError => e
|
|
774
780
|
Log.warn(:worker, "anytime_command_failed", line: command.line, error: e.class.name, msg: e.message)
|
|
775
781
|
end
|
|
@@ -777,7 +783,9 @@ module Samagotchi
|
|
|
777
783
|
|
|
778
784
|
# @param anytime [Boolean] an anytime command's: its line was shown at
|
|
779
785
|
# its command_queued already
|
|
780
|
-
|
|
786
|
+
# @param confirm [CacheConfirm::Confirm, nil] the change asks first: the
|
|
787
|
+
# line that applies it (the TUI's prefill, the web's Apply button)
|
|
788
|
+
def announce_command(command, status:, output:, changed:, anytime: false, confirm: nil)
|
|
781
789
|
text = output.to_s
|
|
782
790
|
event = { type: :command_ran, **command.event_fields, status: status, output: text[0, COMMAND_OUTPUT_LIMIT],
|
|
783
791
|
changed: changed.map(&:to_s), model_name: @engine.effective_model_name }
|
|
@@ -788,6 +796,10 @@ module Samagotchi
|
|
|
788
796
|
event[:llm_context] = @engine.llm_context_summary if changed.intersect?(%i[llm_context model])
|
|
789
797
|
# /thinking or /model: the level the next turn runs at (the web's chip).
|
|
790
798
|
event[:thinking] = @engine.thinking_explained&.summary if changed.intersect?(%i[thinking model])
|
|
799
|
+
# /reasoning-history or /model: the level the next turn runs under
|
|
800
|
+
# (the web's chip); nil on a native host, which ignores it.
|
|
801
|
+
event[:reasoning_history] = @engine.reasoning_history_explained&.summary if changed.intersect?(%i[reasoning_history model])
|
|
802
|
+
event[:confirm] = confirm.to_h if confirm
|
|
791
803
|
event[:output_truncated] = true if text.length > COMMAND_OUTPUT_LIMIT
|
|
792
804
|
@engine.announce(event)
|
|
793
805
|
end
|
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.56.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -212,6 +212,7 @@ files:
|
|
|
212
212
|
- lib/samagotchi/bundles/system/manifest.yml
|
|
213
213
|
- lib/samagotchi/bundles/system/memory_guide.md
|
|
214
214
|
- lib/samagotchi/bundles/system/self_map.md
|
|
215
|
+
- lib/samagotchi/cache_confirm.rb
|
|
215
216
|
- lib/samagotchi/cancellation_controller.rb
|
|
216
217
|
- lib/samagotchi/child_move.rb
|
|
217
218
|
- lib/samagotchi/child_reports.rb
|
|
@@ -260,6 +261,7 @@ files:
|
|
|
260
261
|
- lib/samagotchi/engine_peers.rb
|
|
261
262
|
- lib/samagotchi/events.rb
|
|
262
263
|
- lib/samagotchi/extension_load.rb
|
|
264
|
+
- lib/samagotchi/file_ref.rb
|
|
263
265
|
- lib/samagotchi/gem_update.rb
|
|
264
266
|
- lib/samagotchi/generation_phase.rb
|
|
265
267
|
- lib/samagotchi/git_head.rb
|
|
@@ -317,6 +319,7 @@ files:
|
|
|
317
319
|
- lib/samagotchi/llm/native_tool_normalizer.rb
|
|
318
320
|
- lib/samagotchi/llm/openai_chat.rb
|
|
319
321
|
- lib/samagotchi/llm/prompt_cache.rb
|
|
322
|
+
- lib/samagotchi/llm/stream_drops.rb
|
|
320
323
|
- lib/samagotchi/llm/turn_settings.rb
|
|
321
324
|
- lib/samagotchi/llm/usage.rb
|
|
322
325
|
- lib/samagotchi/llm/utf8_scrub.rb
|
|
@@ -393,6 +396,8 @@ files:
|
|
|
393
396
|
- lib/samagotchi/prompt_warmup.rb
|
|
394
397
|
- lib/samagotchi/question_desk.rb
|
|
395
398
|
- lib/samagotchi/queued_command.rb
|
|
399
|
+
- lib/samagotchi/reasoning_history.rb
|
|
400
|
+
- lib/samagotchi/reasoning_history_command.rb
|
|
396
401
|
- lib/samagotchi/recap_setup.rb
|
|
397
402
|
- lib/samagotchi/recap_store.rb
|
|
398
403
|
- lib/samagotchi/relay.rb
|
|
@@ -419,6 +424,7 @@ files:
|
|
|
419
424
|
- lib/samagotchi/session_retention.rb
|
|
420
425
|
- lib/samagotchi/session_setup.rb
|
|
421
426
|
- lib/samagotchi/sessions_command.rb
|
|
427
|
+
- lib/samagotchi/setting_explained.rb
|
|
422
428
|
- lib/samagotchi/steer.rb
|
|
423
429
|
- lib/samagotchi/steer_cut.rb
|
|
424
430
|
- lib/samagotchi/system_prompt.rb
|
|
@@ -457,6 +463,7 @@ files:
|
|
|
457
463
|
- lib/samagotchi/tool_declarations.rb
|
|
458
464
|
- lib/samagotchi/tool_ids.rb
|
|
459
465
|
- lib/samagotchi/tool_response.rb
|
|
466
|
+
- lib/samagotchi/tool_row_fields.rb
|
|
460
467
|
- lib/samagotchi/tool_runner.rb
|
|
461
468
|
- lib/samagotchi/tool_view.rb
|
|
462
469
|
- lib/samagotchi/tools/args.rb
|
|
@@ -506,6 +513,7 @@ files:
|
|
|
506
513
|
- lib/samagotchi/waiting_steer.rb
|
|
507
514
|
- lib/samagotchi/web/app.rb
|
|
508
515
|
- lib/samagotchi/web/capabilities.rb
|
|
516
|
+
- lib/samagotchi/web/editor.rb
|
|
509
517
|
- lib/samagotchi/web/info_probe.rb
|
|
510
518
|
- lib/samagotchi/web/lan.rb
|
|
511
519
|
- lib/samagotchi/web/markdown_renderer.rb
|
|
@@ -521,6 +529,8 @@ files:
|
|
|
521
529
|
- lib/samagotchi/web/public/command_complete.js
|
|
522
530
|
- lib/samagotchi/web/public/command_list.js
|
|
523
531
|
- lib/samagotchi/web/public/command_view.js
|
|
532
|
+
- lib/samagotchi/web/public/commands.js
|
|
533
|
+
- lib/samagotchi/web/public/composer.js
|
|
524
534
|
- lib/samagotchi/web/public/composer_size.js
|
|
525
535
|
- lib/samagotchi/web/public/context_chips.js
|
|
526
536
|
- lib/samagotchi/web/public/copy.js
|
|
@@ -532,8 +542,11 @@ files:
|
|
|
532
542
|
- lib/samagotchi/web/public/hold.js
|
|
533
543
|
- lib/samagotchi/web/public/images.js
|
|
534
544
|
- lib/samagotchi/web/public/index.html
|
|
545
|
+
- lib/samagotchi/web/public/info_bar.js
|
|
535
546
|
- lib/samagotchi/web/public/init_row.js
|
|
536
547
|
- lib/samagotchi/web/public/kept_commands.js
|
|
548
|
+
- lib/samagotchi/web/public/level_chip.js
|
|
549
|
+
- lib/samagotchi/web/public/lists_view.js
|
|
537
550
|
- lib/samagotchi/web/public/live_timing.js
|
|
538
551
|
- lib/samagotchi/web/public/live_turn.js
|
|
539
552
|
- lib/samagotchi/web/public/llm_context_chip.js
|
|
@@ -544,6 +557,9 @@ files:
|
|
|
544
557
|
- lib/samagotchi/web/public/prompt_history.js
|
|
545
558
|
- lib/samagotchi/web/public/question_card.js
|
|
546
559
|
- lib/samagotchi/web/public/question_cards.js
|
|
560
|
+
- lib/samagotchi/web/public/reasoning_history_chip.js
|
|
561
|
+
- lib/samagotchi/web/public/refs.js
|
|
562
|
+
- lib/samagotchi/web/public/restore.js
|
|
547
563
|
- lib/samagotchi/web/public/route.js
|
|
548
564
|
- lib/samagotchi/web/public/scope.js
|
|
549
565
|
- lib/samagotchi/web/public/scroll.js
|
|
@@ -552,9 +568,9 @@ files:
|
|
|
552
568
|
- lib/samagotchi/web/public/session_title.js
|
|
553
569
|
- lib/samagotchi/web/public/sessions_list.js
|
|
554
570
|
- lib/samagotchi/web/public/shown_texts.js
|
|
555
|
-
- lib/samagotchi/web/public/stable_html.js
|
|
556
571
|
- lib/samagotchi/web/public/stage_model.js
|
|
557
572
|
- lib/samagotchi/web/public/stage_view.js
|
|
573
|
+
- lib/samagotchi/web/public/stream_handlers.js
|
|
558
574
|
- lib/samagotchi/web/public/strip.js
|
|
559
575
|
- lib/samagotchi/web/public/tally.js
|
|
560
576
|
- lib/samagotchi/web/public/task_stop.js
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// A block redrawn every tick (the info bar, once a second while a turn
|
|
2
|
-
// runs) keeps its nodes: +html+ is written only when it changed, so a
|
|
3
|
-
// shown tooltip stays up and a press on one of its buttons isn't lost to a
|
|
4
|
-
// rewrite between mousedown and mouseup. +live+ ({selector: text}) names
|
|
5
|
-
// the parts that change every tick (a running clock): +html+ carries them
|
|
6
|
-
// empty and only their text is set, so a tick alone rewrites nothing else.
|
|
7
|
-
const shown = new WeakMap();
|
|
8
|
-
|
|
9
|
-
export function renderStable(el, html, live = {}) {
|
|
10
|
-
if (shown.get(el) !== html) {
|
|
11
|
-
el.innerHTML = html;
|
|
12
|
-
shown.set(el, html);
|
|
13
|
-
}
|
|
14
|
-
for (const [selector, text] of Object.entries(live)) {
|
|
15
|
-
const part = el.querySelector(selector);
|
|
16
|
-
if (part && part.textContent !== text) part.textContent = text;
|
|
17
|
-
}
|
|
18
|
-
}
|