samagotchi 0.29.0 → 0.30.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 +54 -1
- data/docs/cli.md +4 -4
- data/docs/hooks.md +1 -1
- data/docs/plugins.md +1 -1
- data/docs/sessions.md +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +4 -3
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +17 -4
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/command_steps.rb +35 -15
- data/lib/samagotchi/host_registry.rb +4 -0
- data/lib/samagotchi/kernel_loop.rb +19 -6
- data/lib/samagotchi/llm/chat_loop.rb +12 -6
- data/lib/samagotchi/model_list_store.rb +117 -0
- data/lib/samagotchi/model_profile.rb +43 -0
- data/lib/samagotchi/send_command.rb +5 -3
- data/lib/samagotchi/session.rb +5 -0
- data/lib/samagotchi/session_manager.rb +9 -2
- data/lib/samagotchi/session_metrics.rb +6 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -4
- data/lib/samagotchi/terminal_ui.rb +35 -5
- data/lib/samagotchi/tool_activity.rb +6 -2
- data/lib/samagotchi/tools/delegate.rb +14 -10
- data/lib/samagotchi/tools/execute.rb +3 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +2 -1
- data/lib/samagotchi/web/public/app.js +9 -7
- data/lib/samagotchi/web/public/command_view.js +19 -5
- data/lib/samagotchi/web/public/index.html +14 -6
- data/lib/samagotchi/web/public/stage_model.js +18 -0
- data/lib/samagotchi/web/public/stage_view.js +16 -11
- data/lib/samagotchi/web/public/task_stop.js +11 -2
- data/lib/samagotchi/web/public/timing.js +12 -7
- data/lib/samagotchi/web/public/turn_events.js +3 -1
- data/lib/samagotchi/web/public/turn_model.js +16 -11
- data/lib/samagotchi/web/public/turn_view.js +26 -3
- data/lib/samagotchi/worker.rb +21 -13
- metadata +2 -1
|
@@ -14,10 +14,12 @@ export function turnOutput(data) {
|
|
|
14
14
|
|
|
15
15
|
// The end line for a turn whose worker went away mid-turn (the footer's
|
|
16
16
|
// stop, a crash): no turn_canceled comes, so the page ends the turn itself.
|
|
17
|
+
// The footer's stop is the user's, drawn as a cancel is (\u25A0, timing.js
|
|
18
|
+
// cancelLineText); a worker that exited on its own is a failure (\u2715).
|
|
17
19
|
// null when no turn was running.
|
|
18
20
|
export function workerGoneText({ turnRunning, stopped = false }) {
|
|
19
21
|
if (!turnRunning) return null;
|
|
20
|
-
return
|
|
22
|
+
return stopped ? "\u25A0 canceled (session stopped)" : "\u2715 canceled (worker exited)";
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
// How long a turn's answer waits for the after_turn hooks' answer_display
|
|
@@ -176,21 +176,26 @@ export function failedCount(rows) {
|
|
|
176
176
|
// call's title ("edit lib/a.rb"), else the tools it worked with, else
|
|
177
177
|
// "thinking"; then its call count and how many failed.
|
|
178
178
|
export function genLabel(gen) {
|
|
179
|
+
const { head, calls } = genLabelParts(gen);
|
|
180
|
+
return calls ? `${head} · ${calls}` : head;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// genLabel in its parts: +head+, +calls+ ("3 tool calls (1 failed)", or ""
|
|
184
|
+
// without calls) and +repeats+: the head is words the open step's body
|
|
185
|
+
// starts with (its narration, its first call's title), so an open step's
|
|
186
|
+
// summary can say only its calls (turn_view.js labelHtml).
|
|
187
|
+
export function genLabelParts(gen) {
|
|
179
188
|
const line = String(gen.text || "").trim().split("\n")[0].trim();
|
|
180
|
-
const calls = gen.tools.length;
|
|
189
|
+
const calls = gen.tools.length ? toolCallsText(gen.tools) : "";
|
|
181
190
|
const first = gen.tools[0];
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
} else if (first?.title) {
|
|
186
|
-
head = cut(`${toolName(first)} ${first.title}`);
|
|
187
|
-
} else if (calls) {
|
|
191
|
+
if (line) return { head: cut(line), calls, repeats: true };
|
|
192
|
+
if (first?.title) return { head: cut(`${toolName(first)} ${first.title}`), calls, repeats: true };
|
|
193
|
+
if (gen.tools.length) {
|
|
188
194
|
const names = [...new Set(gen.tools.map(toolName))];
|
|
189
|
-
head = `working with ${names.slice(0, LABEL_TOOLS).join(", ")}${names.length > LABEL_TOOLS ? ", …" : ""}`;
|
|
190
|
-
|
|
191
|
-
return "thinking";
|
|
195
|
+
const head = `working with ${names.slice(0, LABEL_TOOLS).join(", ")}${names.length > LABEL_TOOLS ? ", …" : ""}`;
|
|
196
|
+
return { head, calls, repeats: false };
|
|
192
197
|
}
|
|
193
|
-
return
|
|
198
|
+
return { head: "thinking", calls: "", repeats: false };
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
// The block's summary line: "working · 3 steps · 8 tool calls · execute ×7 · …"
|
|
@@ -399,7 +399,7 @@ export function createTurnView({
|
|
|
399
399
|
// an ellipsed label): at close, and again when a late tool result lands
|
|
400
400
|
// in a step that already closed.
|
|
401
401
|
function writeClosedLabel(entry, gen) {
|
|
402
|
-
entry.summary
|
|
402
|
+
setLabel(entry.summary, gen);
|
|
403
403
|
entry.el.classList.toggle("has-failed", model.failedCount(gen.tools) > 0);
|
|
404
404
|
}
|
|
405
405
|
|
|
@@ -551,7 +551,7 @@ export function createTurnView({
|
|
|
551
551
|
}
|
|
552
552
|
entry.text = document.createElement("div");
|
|
553
553
|
entry.text.className = "gen-text";
|
|
554
|
-
entry.summary
|
|
554
|
+
setLabel(entry.summary, { ...gen, text: "" });
|
|
555
555
|
model.takeAnswer(turn, gen);
|
|
556
556
|
// Cards and hook notices in a step that goes (below, or with the whole
|
|
557
557
|
// block for a plain answer) stay: before the answer, out of the block,
|
|
@@ -731,6 +731,29 @@ function defaultPlaceAnswer(appendToHistory) {
|
|
|
731
731
|
};
|
|
732
732
|
}
|
|
733
733
|
|
|
734
|
+
// A closed step's summary (model.genLabel). When its head is the words the
|
|
735
|
+
// body starts with (the narration's first line, the first call's title)
|
|
736
|
+
// and it has calls, the summary is marked .repeats and its parts are
|
|
737
|
+
// spans: an open step shows only its calls (index.html), so it doesn't say
|
|
738
|
+
// the same words twice. Its text stays genLabel's either way.
|
|
739
|
+
// { repeats, html } (the summary's inside).
|
|
740
|
+
function labelParts(gen) {
|
|
741
|
+
const { head, calls, repeats } = model.genLabelParts(gen);
|
|
742
|
+
if (!repeats || !calls) return { repeats: false, html: escapeHtml(model.genLabel(gen)) };
|
|
743
|
+
return { repeats: true, html: `<span class="gen-head">${escapeHtml(head)}</span><span class="gen-sep"> · </span><span class="gen-calls">${escapeHtml(calls)}</span>` };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function labelHtml(gen) {
|
|
747
|
+
const { repeats, html } = labelParts(gen);
|
|
748
|
+
return `<summary${repeats ? ' class="repeats"' : ""}>${html}</summary>`;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function setLabel(summary, gen) {
|
|
752
|
+
const { repeats, html } = labelParts(gen);
|
|
753
|
+
summary.classList.toggle("repeats", repeats);
|
|
754
|
+
summary.innerHTML = html;
|
|
755
|
+
}
|
|
756
|
+
|
|
734
757
|
// A reloaded tool row, the live row's markup (fillActivityRow): status, tool, params, the full command (an
|
|
735
758
|
// execute/task_create's view), output (its "[tool]" tag dropped, 300 chars, the rest on hover), plus the call's
|
|
736
759
|
// duration from its record.
|
|
@@ -811,7 +834,7 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
811
834
|
const steers = (group.steps[k].steers || []).map((i) => steerHtml(i, false)).join("");
|
|
812
835
|
const tools = gen.tools.length ? `<div class="activity-body">${gen.tools.map((r) => reloadRowHtml(r, thumbs)).join("")}</div>` : "";
|
|
813
836
|
const failed = model.failedCount(gen.tools) ? " has-failed" : "";
|
|
814
|
-
return `<details class="gen${failed}"
|
|
837
|
+
return `<details class="gen${failed}">${labelHtml(gen)}${steers}${thinking}${text}${tools}</details>`;
|
|
815
838
|
});
|
|
816
839
|
parts.push(`<details class="turn-work done"><summary>${escapeHtml(model.blockSummary({ gens, ended: true }))}</summary>${rows.join("")}</details>`);
|
|
817
840
|
}
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -268,7 +268,7 @@ module Samagotchi
|
|
|
268
268
|
|
|
269
269
|
Log.info(:worker, "dead_question_dropped", id: @session.pending_question[:id])
|
|
270
270
|
@session.pending_question = nil
|
|
271
|
-
@session.save(state_dir: @state_dir)
|
|
271
|
+
save_or_log(:dead_question) { @session.save(state_dir: @state_dir) }
|
|
272
272
|
end
|
|
273
273
|
|
|
274
274
|
# spawn_session hands the first prompt over in last_prompt, but
|
|
@@ -285,7 +285,7 @@ module Samagotchi
|
|
|
285
285
|
|
|
286
286
|
prompt = @session.last_prompt
|
|
287
287
|
@session.last_prompt = ""
|
|
288
|
-
@session.save(state_dir: @state_dir)
|
|
288
|
+
save_or_log(:initial_prompt) { @session.save(state_dir: @state_dir) }
|
|
289
289
|
prompt
|
|
290
290
|
end
|
|
291
291
|
|
|
@@ -308,14 +308,16 @@ module Samagotchi
|
|
|
308
308
|
return false unless queue_as_command(prompt, nil)
|
|
309
309
|
|
|
310
310
|
@session.status = Session::STATUS_IDLE
|
|
311
|
-
save_session
|
|
311
|
+
save_or_log(:initial_command) { save_session }
|
|
312
312
|
true
|
|
313
313
|
end
|
|
314
314
|
|
|
315
315
|
# Add the queued context notes to the conversation (between turns only,
|
|
316
316
|
# on this thread), save, then delete their files: a crash before the
|
|
317
317
|
# delete leaves them claimed, and Engine#add_context_note skips a note
|
|
318
|
-
# the saved conversation already holds.
|
|
318
|
+
# the saved conversation already holds. A failed save keeps the files
|
|
319
|
+
# too: the next pass claims them again, finds them in the conversation
|
|
320
|
+
# and saves again. Not activity: a note alone
|
|
319
321
|
# neither starts a turn nor keeps an idle worker up.
|
|
320
322
|
def absorb_notes
|
|
321
323
|
files = SessionInbox.find_new_note_files(@session_dir)
|
|
@@ -326,7 +328,8 @@ module Samagotchi
|
|
|
326
328
|
note = SessionInbox.read_note(file)
|
|
327
329
|
@engine.add_context_note(@session, note) if note
|
|
328
330
|
end
|
|
329
|
-
@session.save(state_dir: @state_dir)
|
|
331
|
+
return unless save_or_log(:notes) { @session.save(state_dir: @state_dir) }
|
|
332
|
+
|
|
330
333
|
claimed.each { |file| FileUtils.rm_f(file) }
|
|
331
334
|
end
|
|
332
335
|
|
|
@@ -426,7 +429,7 @@ module Samagotchi
|
|
|
426
429
|
announce_command(command, status: result.status.to_s, output: result.output, changed: Array(result.changed))
|
|
427
430
|
shown.each { |event| @engine.announce(event) }
|
|
428
431
|
end
|
|
429
|
-
save_session unless Array(result.changed).empty?
|
|
432
|
+
save_or_log(:command) { save_session } unless Array(result.changed).empty?
|
|
430
433
|
user_input(command[:client_id]) if resolved
|
|
431
434
|
@continue_offer.after_command(result, resolved: resolved)
|
|
432
435
|
@continue_offer.run_continue_turn(command) if result.resume
|
|
@@ -515,16 +518,21 @@ module Samagotchi
|
|
|
515
518
|
unless response.nil? || response.strip.empty? || @continue_offer.awaiting?
|
|
516
519
|
SessionInbox.write_output(@session_dir, response)
|
|
517
520
|
end
|
|
518
|
-
|
|
521
|
+
save_or_log(:turn) { save_session }
|
|
519
522
|
end
|
|
520
523
|
|
|
521
|
-
#
|
|
522
|
-
# memory, so
|
|
523
|
-
# save writes
|
|
524
|
-
|
|
525
|
-
|
|
524
|
+
# A save between turns (the turn's own, a command's, the notes', the
|
|
525
|
+
# first prompt's): what it saves is in memory, so one that fails (disk
|
|
526
|
+
# full, permissions; the next save writes it) is logged and must not
|
|
527
|
+
# take the worker down with it.
|
|
528
|
+
# @param at [Symbol] which save, for the log
|
|
529
|
+
# @return [Boolean] whether it saved
|
|
530
|
+
def save_or_log(at)
|
|
531
|
+
yield
|
|
532
|
+
true
|
|
526
533
|
rescue SystemCallError, IOError => e
|
|
527
|
-
Log.exception(:worker, "save_failed", e)
|
|
534
|
+
Log.exception(:worker, "save_failed", e, at: at)
|
|
535
|
+
false
|
|
528
536
|
end
|
|
529
537
|
|
|
530
538
|
# Back to the conversation before the failed turn, as the REPL does (so
|
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.30.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -301,6 +301,7 @@ files:
|
|
|
301
301
|
- lib/samagotchi/memory_bundle/uninstaller.rb
|
|
302
302
|
- lib/samagotchi/memory_paths.rb
|
|
303
303
|
- lib/samagotchi/model_catalog.rb
|
|
304
|
+
- lib/samagotchi/model_list_store.rb
|
|
304
305
|
- lib/samagotchi/model_overlay.rb
|
|
305
306
|
- lib/samagotchi/model_profile.rb
|
|
306
307
|
- lib/samagotchi/model_ref.rb
|