samagotchi 0.49.0 → 0.51.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 +84 -1
- data/bin/chi +2 -2
- data/docs/cli.md +33 -8
- data/docs/configuration.md +11 -9
- data/docs/hooks.md +12 -3
- data/docs/internals/engine.md +4 -2
- data/docs/sessions.md +8 -4
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
- data/lib/samagotchi/bridge.rb +24 -9
- data/lib/samagotchi/bridge_client.rb +3 -1
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +13 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/delivery.rb +33 -0
- data/lib/samagotchi/engine.rb +1 -1
- data/lib/samagotchi/hooks/stream_watch.rb +9 -2
- data/lib/samagotchi/host_registry.rb +17 -4
- data/lib/samagotchi/kernel_loop.rb +28 -2
- data/lib/samagotchi/llm/native_tool_normalizer.rb +2 -2
- data/lib/samagotchi/send_command.rb +124 -10
- data/lib/samagotchi/session_commands.rb +10 -2
- data/lib/samagotchi/session_inbox.rb +27 -7
- data/lib/samagotchi/session_manager.rb +13 -3
- data/lib/samagotchi/session_metrics.rb +32 -6
- data/lib/samagotchi/sessions_command.rb +84 -7
- data/lib/samagotchi/steer_cut.rb +12 -8
- data/lib/samagotchi/terminal_ui/attached_loop.rb +50 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +4 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +68 -6
- data/lib/samagotchi/terminal_ui.rb +48 -5
- data/lib/samagotchi/tool_activity.rb +3 -0
- data/lib/samagotchi/tool_runner.rb +2 -0
- data/lib/samagotchi/tools/builtin_calls.rb +33 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -2
- data/lib/samagotchi/web/message_parts.rb +3 -0
- data/lib/samagotchi/web/public/activity.js +6 -0
- data/lib/samagotchi/web/public/app.js +71 -29
- data/lib/samagotchi/web/public/index.html +17 -15
- data/lib/samagotchi/web/public/live_timing.js +32 -5
- data/lib/samagotchi/web/public/session_title.js +26 -0
- data/lib/samagotchi/web/public/stage_model.js +15 -0
- data/lib/samagotchi/web/public/stage_view.js +10 -4
- data/lib/samagotchi/web/public/strip.js +4 -6
- data/lib/samagotchi/web/public/timing.js +11 -0
- data/lib/samagotchi/web/public/turn_events.js +14 -2
- data/lib/samagotchi/web/public/turn_html.js +24 -1
- data/lib/samagotchi/web/public/turn_view.js +9 -3
- data/lib/samagotchi/web/session_hub.rb +11 -3
- data/lib/samagotchi/worker.rb +1 -1
- data/lib/samagotchi/worker_inbound.rb +8 -5
- data/lib/samagotchi.rb +1 -0
- metadata +3 -1
|
@@ -370,6 +370,17 @@ export function commandView(event, myId) {
|
|
|
370
370
|
};
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
// Whether a command_ran that re-read the conversation (`resync`) needs its
|
|
374
|
+
// own bubble: the redrawn history ends with a command bubble for its line
|
|
375
|
+
// (a `!cmd` appends its "!(<command>)\n<output>" message, which renders as
|
|
376
|
+
// one), so a second bubble would show the output twice. +historyBubbles+:
|
|
377
|
+
// the `.bubble.command` elements in document order; +line+: the event's
|
|
378
|
+
// line. Only the last one counts: it is the redrawn history's end.
|
|
379
|
+
export function historyHasCommandBubble(historyBubbles, line) {
|
|
380
|
+
const last = historyBubbles[historyBubbles.length - 1];
|
|
381
|
+
return !!last && last.querySelector(":scope > .command-line")?.textContent === line;
|
|
382
|
+
}
|
|
383
|
+
|
|
373
384
|
// What a prompt_restored (the worker rolled a failed turn back and handed its
|
|
374
385
|
// prompt back) does in this tab: `refill` is the text for an empty composer,
|
|
375
386
|
// only for a prompt this page sent (`sentIds`, its acks' enqueued_ids), so a
|
|
@@ -547,8 +558,9 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
|
|
|
547
558
|
const call = { iteration: part.iteration, call_index: part.call_index, tool: part.tool };
|
|
548
559
|
if (part.label) call.label = part.label;
|
|
549
560
|
const title = part.title ? { title: part.title } : {};
|
|
561
|
+
const alias = part.called_as ? { called_as: part.called_as } : {};
|
|
550
562
|
const view = part.view ? { view: part.view } : {};
|
|
551
|
-
events.push({ type: "tool_call_started", ...call, params: part.params, ...title, ...view });
|
|
563
|
+
events.push({ type: "tool_call_started", ...call, params: part.params, ...title, ...alias, ...view });
|
|
552
564
|
if (part.status !== "running") {
|
|
553
565
|
// The activity as the live event had it; action and duration_ms
|
|
554
566
|
// only in snapshots from a worker that keeps them (the TUI's
|
|
@@ -556,7 +568,7 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
|
|
|
556
568
|
const action = part.action ? { action: part.action } : {};
|
|
557
569
|
const completed = {
|
|
558
570
|
type: "tool_call_completed", ...call, output: part.output, output_truncated: !!part.output_truncated,
|
|
559
|
-
activity: { ...action, tool: part.tool, status: part.status, params: part.params, ...title },
|
|
571
|
+
activity: { ...action, tool: part.tool, status: part.status, params: part.params, ...title, ...alias },
|
|
560
572
|
};
|
|
561
573
|
if (part.duration_ms != null) completed.duration_ms = part.duration_ms;
|
|
562
574
|
if (part.diff) completed.diff = part.diff;
|
|
@@ -78,13 +78,15 @@ export function toolRowInnerHtml(row, { thumbs = () => "" } = {}) {
|
|
|
78
78
|
const shown = row.title || row.params;
|
|
79
79
|
const hover = rowHover(row) ? ` title="${escapeHtml(rowHover(row))}"` : "";
|
|
80
80
|
const params = shown ? `<span class="activity-params"${hover}>${escapeHtml(shown)}</span>` : "";
|
|
81
|
+
// The name the model called the tool by when it was an alias's, dim.
|
|
82
|
+
const alias = row.called_as ? `<span class="activity-alias">(called as ${escapeHtml(row.called_as)})</span>` : "";
|
|
81
83
|
const duration = formatDuration(row.duration_ms);
|
|
82
84
|
const full = row.status === "running" ? "" : String(row.output || "").replace(/^\[[^\]]*\]\s*/, "");
|
|
83
85
|
const output = full
|
|
84
86
|
? `<div class="activity-output" title="${escapeHtml(full)}">${escapeHtml(full.length > 300 ? `${full.slice(0, 300)}…` : full)}</div>`
|
|
85
87
|
: "";
|
|
86
88
|
const status = statusParts(row.status);
|
|
87
|
-
return `${statusMarkHtml(status)}<span class="activity-tool">${escapeHtml(toolName(row))}</span>${params}${statusWordHtml(status)}` +
|
|
89
|
+
return `${statusMarkHtml(status)}<span class="activity-tool">${escapeHtml(toolName(row))}</span>${params}${alias}${statusWordHtml(status)}` +
|
|
88
90
|
`${duration ? `<span class="activity-duration">${escapeHtml(duration)}</span>` : ""}` +
|
|
89
91
|
`${commandBlockHtml(row.view, row.title)}${llmEditHtml(row)}${output}` +
|
|
90
92
|
`${row.images?.length ? thumbs(row.images) : ""}${row.diff ? activityDiffHtml(row.diff) : ""}`;
|
|
@@ -99,6 +101,27 @@ export function stateBadgeHtml(state) {
|
|
|
99
101
|
return BADGES[state] ? `<span class="state-badge">${escapeHtml(BADGES[state])}</span>` : "";
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
// A `!cmd`'s saved user message (the worker appends "!(<command>)\n<output>"
|
|
105
|
+
// for the next turn): its command line and its output, or null when the
|
|
106
|
+
// content is any other message. The command sits on the first line inside
|
|
107
|
+
// `!(` `)`; the output is everything after that first newline. A message
|
|
108
|
+
// that merely starts with "!" or "!(" without the newline shape is a plain
|
|
109
|
+
// message.
|
|
110
|
+
export function shellCommandMessage(content) {
|
|
111
|
+
const text = String(content ?? "");
|
|
112
|
+
const match = text.match(/^!\((.*)\)\n([\s\S]*)$/);
|
|
113
|
+
if (!match) return null;
|
|
114
|
+
return { line: `!${match[1]}`, text: match[2] };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A `!cmd`'s history bubble: the same markup the live page draws for a
|
|
118
|
+
// command (app.js's drawCommandBubble): the line, the output in a
|
|
119
|
+
// pre.command-output.
|
|
120
|
+
export function commandBubbleHtml({ line, text }) {
|
|
121
|
+
const body = text ? `<pre class="command-output">${escapeHtml(text)}</pre>` : "";
|
|
122
|
+
return `<div class="bubble command"><div class="command-line">${escapeHtml(line)}</div>${body}</div>`;
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
// A user's line: who sent it (+label+, a delegate report's or another
|
|
103
126
|
// client's), its body (`>` lines as quotes) and +thumbs+ (its images'
|
|
104
127
|
// markup), its state's badge. +message+: { content, display } (the copy
|
|
@@ -15,7 +15,7 @@ import { escapeHtml, failedTurnText, messageBodyHtml, noteHtml, normalize, sourc
|
|
|
15
15
|
import { thumbsHtml } from "./images.js";
|
|
16
16
|
import { shouldFollowScroll } from "./scroll.js";
|
|
17
17
|
import { hookNoticeLabel } from "./turn_events.js";
|
|
18
|
-
import { emptyAnswerHtml, htmlElement, toolRowHtml, toolRowInnerHtml, userBubbleHtml } from "./turn_html.js";
|
|
18
|
+
import { commandBubbleHtml, emptyAnswerHtml, htmlElement, shellCommandMessage, toolRowHtml, toolRowInnerHtml, userBubbleHtml } from "./turn_html.js";
|
|
19
19
|
import { cancelLineHtml, formatDuration, turnGroups, turnTimingText } from "./timing.js";
|
|
20
20
|
import { createSentenceFeed } from "./sentences.js";
|
|
21
21
|
import { createTicker } from "./thinking_ticker.js";
|
|
@@ -755,8 +755,14 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
755
755
|
const { body, renderedMarkdown } = messageBodyHtml(m);
|
|
756
756
|
return `<div class="bubble output${renderedMarkdown ? " markdown" : ""}"${copySourceAttr(m, renderedMarkdown)}>${body}</div>${timing}`;
|
|
757
757
|
};
|
|
758
|
-
// A user's line, labelled when chi brought it in (a delegate report).
|
|
759
|
-
|
|
758
|
+
// A user's line, labelled when chi brought it in (a delegate report). A
|
|
759
|
+
// `!cmd`'s saved message (the worker's "!(<command>)\n<output>") renders
|
|
760
|
+
// as its command bubble, as live does — not as a user bubble.
|
|
761
|
+
const userHtml = (m, opts = {}) => {
|
|
762
|
+
const shell = shellCommandMessage(m?.content);
|
|
763
|
+
if (shell) return commandBubbleHtml(shell);
|
|
764
|
+
return userBubbleHtml(m, { label: sourceLabel(m?.source), thumbs: thumbs(m.images), ...opts });
|
|
765
|
+
};
|
|
760
766
|
// The first text after a delegate report merged mid-turn, unless it is
|
|
761
767
|
// the turn's answer (shown anyway).
|
|
762
768
|
const reportReplyHtml = (i, group) => {
|
|
@@ -307,12 +307,20 @@ module Samagotchi
|
|
|
307
307
|
emit("session", session: summary)
|
|
308
308
|
end
|
|
309
309
|
|
|
310
|
-
# One HostRegistry
|
|
310
|
+
# One HostRegistry kept across scans: every session's ctx_pct may
|
|
311
311
|
# need its llm_context budget resolved, and the registry's config read
|
|
312
312
|
# is what would otherwise repeat per scan and per session. Built on
|
|
313
|
-
# the first summary that needs it.
|
|
313
|
+
# the first summary that needs it, and built again when config.yml's
|
|
314
|
+
# hosts: changed (compared as read, like App#host_registry: the YAML
|
|
315
|
+
# read is mtime-cached), so a host added or a window changed while
|
|
316
|
+
# chi web runs is picked up on the next refresh of a card.
|
|
314
317
|
def hub_registry
|
|
315
|
-
|
|
318
|
+
hosts = ConfigFile.hosts_config
|
|
319
|
+
if @hub_registry.nil? || hosts != @hub_registry_hosts
|
|
320
|
+
@hub_registry = HostRegistry.new(hosts_config: hosts)
|
|
321
|
+
@hub_registry_hosts = hosts
|
|
322
|
+
end
|
|
323
|
+
@hub_registry
|
|
316
324
|
end
|
|
317
325
|
|
|
318
326
|
def drop(id)
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -953,7 +953,7 @@ module Samagotchi
|
|
|
953
953
|
# A line with images runs as its own next turn (steering merges text
|
|
954
954
|
# only), and so does anything queued after it, to keep the order.
|
|
955
955
|
files = SessionInbox.find_new_input_files(@session_dir).sort
|
|
956
|
-
.take_while { |input_file| !SessionInbox.
|
|
956
|
+
.take_while { |input_file| !SessionInbox.waits_for_turn_end?(input_file) }
|
|
957
957
|
merged = files.filter_map do |input_file|
|
|
958
958
|
claimed_file = SessionInbox.claim_input_file(input_file)
|
|
959
959
|
next unless claimed_file
|
|
@@ -31,9 +31,11 @@ module Samagotchi
|
|
|
31
31
|
|
|
32
32
|
# A prompt to run a turn for: an input file's, or the first prompt.
|
|
33
33
|
# +no_interrupt+: an offer its turn makes keeps it for its continue
|
|
34
|
-
# turn; +images+: its image refs ({file:, name:})
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
# turn; +images+: its image refs ({file:, name:}); +delivery+: the wire
|
|
35
|
+
# value the sender asked for ("queue", or nil for the default
|
|
36
|
+
# next_step).
|
|
37
|
+
Prompt = Data.define(:text, :origin, :no_interrupt, :images, :delivery) do
|
|
38
|
+
def initialize(text:, origin: nil, no_interrupt: false, images: [], delivery: nil) = super
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
# A context wake turn to run: the ContextAbsorber::Delivery that woke
|
|
@@ -181,11 +183,12 @@ module Samagotchi
|
|
|
181
183
|
return unless claimed_file
|
|
182
184
|
|
|
183
185
|
begin
|
|
184
|
-
message, origin, no_interrupt, images = SessionInbox.read_input(claimed_file)
|
|
186
|
+
message, origin, no_interrupt, images, delivery = SessionInbox.read_input(claimed_file)
|
|
185
187
|
return if message.to_s.strip.empty?
|
|
186
188
|
return if Array(images).empty? && queue_as_command(message, origin, File.basename(input_file))
|
|
187
189
|
|
|
188
|
-
yield Prompt.new(text: message, origin: origin, no_interrupt: !!no_interrupt, images: images || []
|
|
190
|
+
yield Prompt.new(text: message, origin: origin, no_interrupt: !!no_interrupt, images: images || [],
|
|
191
|
+
delivery: delivery)
|
|
189
192
|
ensure
|
|
190
193
|
FileUtils.rm_f(claimed_file)
|
|
191
194
|
end
|
data/lib/samagotchi.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "samagotchi/model_profile"
|
|
|
6
6
|
require_relative "samagotchi/prompt"
|
|
7
7
|
require_relative "samagotchi/client"
|
|
8
8
|
require_relative "samagotchi/debug_log"
|
|
9
|
+
require_relative "samagotchi/delivery"
|
|
9
10
|
require_relative "samagotchi/pending_input_queue"
|
|
10
11
|
require_relative "samagotchi/kernel_loop"
|
|
11
12
|
require_relative "samagotchi/session"
|
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.51.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -238,6 +238,7 @@ files:
|
|
|
238
238
|
- lib/samagotchi/continue_offer.rb
|
|
239
239
|
- lib/samagotchi/cut_policy.rb
|
|
240
240
|
- lib/samagotchi/debug_log.rb
|
|
241
|
+
- lib/samagotchi/delivery.rb
|
|
241
242
|
- lib/samagotchi/desktop.rb
|
|
242
243
|
- lib/samagotchi/desktop/macos.rb
|
|
243
244
|
- lib/samagotchi/desktop/macos/App.swift
|
|
@@ -533,6 +534,7 @@ files:
|
|
|
533
534
|
- lib/samagotchi/web/public/scroll.js
|
|
534
535
|
- lib/samagotchi/web/public/sentences.js
|
|
535
536
|
- lib/samagotchi/web/public/session_state.js
|
|
537
|
+
- lib/samagotchi/web/public/session_title.js
|
|
536
538
|
- lib/samagotchi/web/public/sessions_list.js
|
|
537
539
|
- lib/samagotchi/web/public/shown_texts.js
|
|
538
540
|
- lib/samagotchi/web/public/stable_html.js
|