samagotchi 0.40.0 → 0.42.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 +82 -1
- data/README.md +1 -1
- data/bin/chi +19 -2
- data/docs/cli.md +32 -7
- data/docs/configuration.md +34 -3
- data/docs/guardrails.md +15 -3
- data/docs/internals/prompt-caching.md +2 -1
- data/docs/memory.md +108 -1
- data/docs/sessions.md +5 -5
- data/docs/testing.md +16 -0
- data/lib/samagotchi/bridge/card_store.rb +6 -7
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +23 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +5 -5
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +13 -0
- data/lib/samagotchi/bundles/system/self_map.md +4 -2
- data/lib/samagotchi/child_reports.rb +8 -4
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_command.rb +5 -2
- data/lib/samagotchi/engine.rb +121 -16
- data/lib/samagotchi/events.rb +8 -1
- data/lib/samagotchi/guardrails/model_size.rb +3 -4
- data/lib/samagotchi/guardrails/rules.rb +27 -17
- data/lib/samagotchi/guardrails/targets.rb +40 -1
- data/lib/samagotchi/guardrails/verdict.rb +6 -3
- data/lib/samagotchi/llm/errors.rb +33 -1
- data/lib/samagotchi/llm_context_command.rb +151 -0
- data/lib/samagotchi/llm_context_override.rb +180 -0
- data/lib/samagotchi/llm_context_strategy.rb +63 -13
- data/lib/samagotchi/memory_bundle/index_size.rb +103 -0
- data/lib/samagotchi/memory_bundle/index_sync.rb +3 -2
- data/lib/samagotchi/model_match.rb +54 -0
- data/lib/samagotchi/model_notes.rb +205 -0
- data/lib/samagotchi/parent_report.rb +12 -2
- data/lib/samagotchi/plugin/context.rb +3 -2
- data/lib/samagotchi/plugin/sessions.rb +3 -1
- data/lib/samagotchi/prompt_note.rb +53 -0
- data/lib/samagotchi/reply_wait.rb +3 -17
- data/lib/samagotchi/self_report.rb +47 -0
- data/lib/samagotchi/send_command.rb +22 -2
- data/lib/samagotchi/session.rb +44 -4
- data/lib/samagotchi/session_commands.rb +45 -6
- data/lib/samagotchi/session_manager.rb +5 -2
- data/lib/samagotchi/session_metrics.rb +47 -7
- data/lib/samagotchi/system_prompt.rb +79 -11
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +13 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +32 -12
- data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +40 -0
- data/lib/samagotchi/terminal_ui.rb +16 -6
- data/lib/samagotchi/tool_runner.rb +26 -1
- data/lib/samagotchi/tools/memory.rb +70 -3
- data/lib/samagotchi/turn_flow.rb +44 -8
- data/lib/samagotchi/turn_note.rb +40 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +35 -6
- data/lib/samagotchi/web/public/app.js +47 -7
- data/lib/samagotchi/web/public/ctx.js +28 -5
- data/lib/samagotchi/web/public/format.js +29 -2
- data/lib/samagotchi/web/public/index.html +12 -0
- data/lib/samagotchi/web/public/live_turn.js +1 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +194 -0
- data/lib/samagotchi/web/public/session_state.js +6 -0
- data/lib/samagotchi/web/public/timing.js +8 -2
- data/lib/samagotchi/web/public/turn_view.js +15 -2
- data/lib/samagotchi/web/session_summary.rb +4 -0
- data/lib/samagotchi/worker.rb +36 -18
- data/lib/samagotchi/worker_wakes.rb +2 -1
- metadata +8 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// The session bar's LLM context chip: the strategy the next turn runs under
|
|
2
|
+
// ("llm ctx stale,forget"; "· session" when the session set its own), and a
|
|
3
|
+
// popover that sets the session's own strategy, apply rule and budget by
|
|
4
|
+
// running /llm-context in its worker (the command's reply shows in the
|
|
5
|
+
// conversation, like a typed one). The values and where each came from are
|
|
6
|
+
// the session's llm_context (LLMContextStrategy::Explained#summary: the
|
|
7
|
+
// worker's snapshot, a command_ran, or the server's own reading of the
|
|
8
|
+
// session file).
|
|
9
|
+
|
|
10
|
+
export const STRATEGIES = [
|
|
11
|
+
{ value: "none", label: "none" },
|
|
12
|
+
{ value: "stale", label: "stale" },
|
|
13
|
+
{ value: "stale,forget", label: "stale + forget (experimental)" },
|
|
14
|
+
];
|
|
15
|
+
export const APPLIES = ["payoff", "next_request", "turn_end"];
|
|
16
|
+
|
|
17
|
+
const DEFAULT = "default";
|
|
18
|
+
const LAYER_ORDER = ["stale", "forget"];
|
|
19
|
+
|
|
20
|
+
// Layers as one value, in chi's order (stale before forget); unknown ones after.
|
|
21
|
+
export function strategyValue(layers) {
|
|
22
|
+
if (!layers.length) return "none";
|
|
23
|
+
const rank = (name) => { const at = LAYER_ORDER.indexOf(name); return at < 0 ? LAYER_ORDER.length : at; };
|
|
24
|
+
return [...layers].sort((a, b) => rank(a) - rank(b)).join(",");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The session's own values as the form shows them: a strategy value, an
|
|
28
|
+
// apply rule and a budget word ("64000", "off"); "default" / "" for unset.
|
|
29
|
+
export function ownValues(summary) {
|
|
30
|
+
const own = summary?.own || {};
|
|
31
|
+
const strategy = Array.isArray(own.strategy) ? strategyValue(own.strategy) : DEFAULT;
|
|
32
|
+
const budget = typeof own.budget_tokens === "number" ? (own.budget_tokens > 0 ? String(own.budget_tokens) : "off") : "";
|
|
33
|
+
return { strategy, apply: own.apply || DEFAULT, budget };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The chip: its text, tooltip and whether the session set its own; null
|
|
37
|
+
// without a summary (an older worker).
|
|
38
|
+
export function chipModel(summary) {
|
|
39
|
+
if (!summary || !summary.strategy) return null;
|
|
40
|
+
const own = summary.strategy_source === "session" || !!summary.own;
|
|
41
|
+
const budget = summary.budget_tokens ? `${summary.budget_tokens} tokens` : "off";
|
|
42
|
+
const title = `LLM context strategy: ${summary.strategy} (${summary.strategy_where}); apply ${summary.apply} ` +
|
|
43
|
+
`(${summary.apply_where}); budget ${budget} (${summary.budget_where}). Click to change it for this session.`;
|
|
44
|
+
return { text: `llm ctx ${summary.strategy}${own ? " · session" : ""}`, title, own };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function chipHtml(summary, escapeHtml) {
|
|
48
|
+
const chip = chipModel(summary);
|
|
49
|
+
if (!chip) return "";
|
|
50
|
+
return `<button type="button" class="model llmctx-chip${chip.own ? " own" : ""}" title="${escapeHtml(chip.title)}" ` +
|
|
51
|
+
`aria-haspopup="dialog">${escapeHtml(chip.text)}</button>`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The /llm-context line for the form's values. With +initial+ (the values
|
|
55
|
+
// the form opened with) only the fields the user changed go, so a value the
|
|
56
|
+
// form can't show is never unset by a Set; null when nothing changed.
|
|
57
|
+
// Without it every field goes, "default" for one left to the model, and all
|
|
58
|
+
// of them default is a reset.
|
|
59
|
+
export function commandLine({ strategy = DEFAULT, apply = DEFAULT, budget = "" } = {}, initial = null) {
|
|
60
|
+
const words = { strategy, apply, budget: String(budget).trim() || DEFAULT };
|
|
61
|
+
const was = initial && { strategy: initial.strategy, apply: initial.apply, budget: String(initial.budget).trim() || DEFAULT };
|
|
62
|
+
const fields = Object.keys(words).filter((field) => !was || words[field] !== was[field]);
|
|
63
|
+
if (!fields.length) return null;
|
|
64
|
+
if (!was && fields.every((field) => words[field] === DEFAULT)) return "/llm-context reset";
|
|
65
|
+
return `/llm-context ${fields.map((field) => `${field} ${words[field]}`).join(" ")}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The strategy list, with the session's own value when it isn't one of them
|
|
69
|
+
// (forget alone, a newer chi's layer).
|
|
70
|
+
export function strategyOptions(chosen) {
|
|
71
|
+
if (chosen === DEFAULT || STRATEGIES.some((s) => s.value === chosen)) return STRATEGIES;
|
|
72
|
+
return [...STRATEGIES, { value: chosen, label: chosen }];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function option(value, label, chosen, escapeHtml) {
|
|
76
|
+
return `<option value="${escapeHtml(value)}"${value === chosen ? " selected" : ""}>${escapeHtml(label)}</option>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The popover's form: the values now (each with its source), and the
|
|
80
|
+
// session's own to change.
|
|
81
|
+
export function nowText(summary) {
|
|
82
|
+
const budget = summary.budget_tokens ? `${summary.budget_tokens} tokens` : "off";
|
|
83
|
+
return `Now: ${summary.strategy} (${summary.strategy_where}) · apply ${summary.apply} (${summary.apply_where}) · ` +
|
|
84
|
+
`budget ${budget} (${summary.budget_where})`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formHtml(summary, escapeHtml, { running = false } = {}) {
|
|
88
|
+
const own = ownValues(summary);
|
|
89
|
+
const follow = (field) => (summary[`${field}_source`] === "session" ? "follow the model" : `follow the model (${summary[field]})`);
|
|
90
|
+
return `<form class="llmctx-form"><div class="ctx-pop-head"><strong>LLM context</strong>` +
|
|
91
|
+
`<span class="ctx-pop-kind">this session</span>` +
|
|
92
|
+
`<button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div>` +
|
|
93
|
+
`<div class="ctx-pop-meta llmctx-now">${escapeHtml(nowText(summary))}</div>` +
|
|
94
|
+
`<label class="llmctx-field"><span>Strategy</span><select name="strategy">` +
|
|
95
|
+
option(DEFAULT, follow("strategy"), own.strategy, escapeHtml) +
|
|
96
|
+
strategyOptions(own.strategy).map((s) => option(s.value, s.label, own.strategy, escapeHtml)).join("") + `</select></label>` +
|
|
97
|
+
`<label class="llmctx-field"><span>Apply</span><select name="apply">` +
|
|
98
|
+
option(DEFAULT, follow("apply"), own.apply, escapeHtml) +
|
|
99
|
+
APPLIES.map((a) => option(a, a, own.apply, escapeHtml)).join("") + `</select></label>` +
|
|
100
|
+
`<label class="llmctx-field"><span>Budget</span><input name="budget" type="text" inputmode="numeric" autocomplete="off" ` +
|
|
101
|
+
`placeholder="follow the model (64000, 64k or off)" value="${escapeHtml(own.budget)}"></label>` +
|
|
102
|
+
`<div class="ctx-pop-meta">From the next turn's start. Turning forget on or off re-reads the whole prompt once.` +
|
|
103
|
+
`${running ? " A turn is running: set it when it ends." : ""}</div>` +
|
|
104
|
+
`<div class="ctx-pop-actions"><button type="button" class="ctx-pop-detach" data-act="reset">follow the model</button>` +
|
|
105
|
+
`<button type="submit" class="llmctx-set"${running ? " disabled" : ""}>Set</button></div></form>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The chip's popover. el: where the chip is drawn (#infoText, redrawn every
|
|
109
|
+
// tick: clicks are delegated); summary(): the open session's llm_context;
|
|
110
|
+
// session(): the open session's id; running(): a turn runs; run(line):
|
|
111
|
+
// sends the command.
|
|
112
|
+
export function createLlmContextControl({ el, escapeHtml, summary, session = () => null, running = () => false, run,
|
|
113
|
+
doc = document, win = window }) {
|
|
114
|
+
const pop = doc.createElement("div");
|
|
115
|
+
pop.id = "llmContextPopover";
|
|
116
|
+
pop.className = "ctx-popover llmctx-popover";
|
|
117
|
+
pop.setAttribute("role", "dialog");
|
|
118
|
+
pop.hidden = true;
|
|
119
|
+
doc.body.appendChild(pop);
|
|
120
|
+
// The values the form opened with, and the session it is for.
|
|
121
|
+
let initial = null;
|
|
122
|
+
let openFor = null;
|
|
123
|
+
|
|
124
|
+
function place(anchor) {
|
|
125
|
+
const rect = anchor.getBoundingClientRect();
|
|
126
|
+
const width = Math.min(360, win.innerWidth - 32);
|
|
127
|
+
pop.style.width = `${width}px`;
|
|
128
|
+
pop.style.left = `${Math.max(16, Math.min(rect.left, win.innerWidth - width - 16))}px`;
|
|
129
|
+
pop.style.bottom = `${Math.max(16, win.innerHeight - rect.top + 8)}px`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function open(anchor) {
|
|
133
|
+
const data = summary();
|
|
134
|
+
if (!data) return;
|
|
135
|
+
initial = ownValues(data);
|
|
136
|
+
openFor = session();
|
|
137
|
+
pop.innerHTML = formHtml(data, escapeHtml, { running: running() });
|
|
138
|
+
pop.hidden = false;
|
|
139
|
+
place(anchor);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function close() {
|
|
143
|
+
pop.hidden = true;
|
|
144
|
+
initial = null;
|
|
145
|
+
openFor = null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isOpen() { return !pop.hidden; }
|
|
149
|
+
|
|
150
|
+
// A new summary (a command_ran, a re-read): an open form keeps what the
|
|
151
|
+
// user typed; only its "Now:" line and Set's state follow. Another
|
|
152
|
+
// session closes it.
|
|
153
|
+
function refresh() {
|
|
154
|
+
if (!isOpen()) return;
|
|
155
|
+
const data = summary();
|
|
156
|
+
if (!data || session() !== openFor) return close();
|
|
157
|
+
const now = pop.querySelector(".llmctx-now");
|
|
158
|
+
if (now) now.textContent = nowText(data);
|
|
159
|
+
const set = pop.querySelector(".llmctx-set");
|
|
160
|
+
if (set) set.disabled = running();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
el.addEventListener("click", (e) => {
|
|
164
|
+
const anchor = e.target.closest(".llmctx-chip");
|
|
165
|
+
if (!anchor) return;
|
|
166
|
+
e.stopPropagation();
|
|
167
|
+
if (isOpen()) close();
|
|
168
|
+
else open(anchor);
|
|
169
|
+
});
|
|
170
|
+
pop.addEventListener("submit", (e) => {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
const form = e.target;
|
|
173
|
+
const line = commandLine({ strategy: form.elements.strategy.value, apply: form.elements.apply.value,
|
|
174
|
+
budget: form.elements.budget.value }, initial);
|
|
175
|
+
close();
|
|
176
|
+
if (line) run(line);
|
|
177
|
+
});
|
|
178
|
+
pop.addEventListener("click", (e) => {
|
|
179
|
+
const act = e.target.closest("[data-act]")?.dataset.act;
|
|
180
|
+
if (act === "close") close();
|
|
181
|
+
else if (act === "reset") {
|
|
182
|
+
close();
|
|
183
|
+
run(commandLine());
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
doc.addEventListener("click", (e) => {
|
|
187
|
+
if (isOpen() && !pop.contains(e.target) && !e.target.closest?.(".llmctx-chip")) close();
|
|
188
|
+
});
|
|
189
|
+
doc.addEventListener("keydown", (e) => {
|
|
190
|
+
if (e.key === "Escape" && isOpen()) close();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return { close, refresh, get open() { return isOpen(); } };
|
|
194
|
+
}
|
|
@@ -14,6 +14,9 @@ export function emptySessionState() {
|
|
|
14
14
|
// in the info bar's tooltip).
|
|
15
15
|
preloadedMemories: [],
|
|
16
16
|
mutedMemories: [],
|
|
17
|
+
// The model notes the session's prompt carried (its prompt_notes: name,
|
|
18
|
+
// scope, chars, digest; the info bar's notes chip).
|
|
19
|
+
promptNotes: [],
|
|
17
20
|
// A delegated session's parent (the info bar's "delegated by" chip).
|
|
18
21
|
parentId: null,
|
|
19
22
|
ctxPct: null,
|
|
@@ -33,6 +36,9 @@ export function emptySessionState() {
|
|
|
33
36
|
// worker_version / worker_features): the info bar's stale-worker badge.
|
|
34
37
|
workerVersion: null,
|
|
35
38
|
workerFeatures: [],
|
|
39
|
+
// The LLM context strategy, apply rule and budget the next turn runs
|
|
40
|
+
// under, each with its source (the info bar's llm ctx chip), or null.
|
|
41
|
+
llmContext: null,
|
|
36
42
|
timing: normalizeTiming(),
|
|
37
43
|
};
|
|
38
44
|
}
|
|
@@ -61,6 +61,9 @@ export function normalizeTiming(data = {}) {
|
|
|
61
61
|
activeTurn: validRecord(data.active_turn) ? data.active_turn : null,
|
|
62
62
|
// The session's token sums and speeds (SessionMetrics' tokens block).
|
|
63
63
|
tokens: validRecord(data.tokens) ? data.tokens : null,
|
|
64
|
+
// The memory indexes the session's prompt holds (SessionMetrics'
|
|
65
|
+
// memory_index), for the ctx tooltip.
|
|
66
|
+
memoryIndex: validRecord(data.memory_index) ? data.memory_index : null,
|
|
64
67
|
};
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -96,7 +99,8 @@ export function mergeTiming(timing, data = {}) {
|
|
|
96
99
|
if (!replaceById(toolRecords, record)) toolRecords.push(record);
|
|
97
100
|
});
|
|
98
101
|
const tokens = fresh.tokens || timing?.tokens || null;
|
|
99
|
-
|
|
102
|
+
const memoryIndex = fresh.memoryIndex || timing?.memoryIndex || null;
|
|
103
|
+
return { timing: { ...fresh, turnRecords, toolRecords, tokens, memoryIndex }, short: turnRecords.length < count };
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
function replaceById(records, record) {
|
|
@@ -229,8 +233,10 @@ function turnGroup(g, items, timing) {
|
|
|
229
233
|
|
|
230
234
|
// A canceled turn that ran more than one step or any call has no answer:
|
|
231
235
|
// its partial text stays in the block, as it does live (turn_view.js
|
|
232
|
-
// turnEnded promotes only a completed turn or a lone step with no tools).
|
|
236
|
+
// turnEnded promotes only a completed turn or a lone step with no tools). A
|
|
237
|
+
// failed turn never answered: its texts are steps.
|
|
233
238
|
function canceledSteps(g, record, tools) {
|
|
239
|
+
if (record?.status === "failed") return true;
|
|
234
240
|
return record?.status === "canceled" && (g.texts.length > 1 || tools.length > 0);
|
|
235
241
|
}
|
|
236
242
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// removed at the end: the answer bubble takes its place.
|
|
12
12
|
import { leavesBlock } from "./card.js";
|
|
13
13
|
import { copySourceAttr } from "./copy.js";
|
|
14
|
-
import { escapeHtml, messageBodyHtml, noteHtml, normalize, sourceLabel, steerRowHtml } from "./format.js";
|
|
14
|
+
import { escapeHtml, failedTurnText, messageBodyHtml, noteHtml, normalize, sourceLabel, steerRowHtml } from "./format.js";
|
|
15
15
|
import { thumbsHtml } from "./images.js";
|
|
16
16
|
import { shouldFollowScroll } from "./scroll.js";
|
|
17
17
|
import { hookNoticeLabel } from "./turn_events.js";
|
|
@@ -701,6 +701,17 @@ function setLabel(summary, gen) {
|
|
|
701
701
|
summary.innerHTML = html;
|
|
702
702
|
}
|
|
703
703
|
|
|
704
|
+
// A re-rendered failed turn's line (a .bubble.cancel, as live's
|
|
705
|
+
// addEndBubble), in turn_failed's words (failedTurnText) from what the turn
|
|
706
|
+
// record kept of it (SessionMetrics' failure: summary, message,
|
|
707
|
+
// error_class, kept_steps); "" for any other record, or a failed one saved
|
|
708
|
+
// before records kept that.
|
|
709
|
+
function failedLineHtml(record) {
|
|
710
|
+
const failure = record?.status === "failed" ? record.failure : null;
|
|
711
|
+
if (!failure || typeof failure !== "object") return "";
|
|
712
|
+
return `<div class="bubble cancel">${escapeHtml(failedTurnText(failure))}</div>`;
|
|
713
|
+
}
|
|
714
|
+
|
|
704
715
|
// A reloaded history in the turn view (renderHistory's object path): per
|
|
705
716
|
// turn the prompt, a collapsed block of its steps when it had any (texts,
|
|
706
717
|
// and with the server's parts the thinking, params and output, from the
|
|
@@ -789,8 +800,10 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
789
800
|
// timing after its prompt and steps, where the live view leaves it.
|
|
790
801
|
else if (group.answer === null) parts.push(timingHtml(group));
|
|
791
802
|
else parts.push(answerHtml(items[group.answer], timingHtml(group)));
|
|
792
|
-
// A canceled turn ends with its cancel line,
|
|
803
|
+
// A canceled turn ends with its cancel line, a failed one whose steps
|
|
804
|
+
// stayed with its failure line, as the live view left them.
|
|
793
805
|
parts.push(cancelLineHtml(group.record, escapeHtml));
|
|
806
|
+
parts.push(failedLineHtml(group.record));
|
|
794
807
|
return parts.join("");
|
|
795
808
|
}).join("");
|
|
796
809
|
}
|
|
@@ -47,6 +47,8 @@ module Samagotchi
|
|
|
47
47
|
# The session's --memory and --mute lists (info-bar tooltip).
|
|
48
48
|
preloaded_memory_names: Array(session.preloaded_memory_names),
|
|
49
49
|
muted_memory_names: Array(session.muted_memory_names),
|
|
50
|
+
# The model notes its prompt carried (the info bar's notes chip).
|
|
51
|
+
prompt_notes: Array(session.prompt_notes).map(&:to_h),
|
|
50
52
|
# The session that delegated this one (the `delegate` tool), else nil.
|
|
51
53
|
parent_id: session.parent_id,
|
|
52
54
|
# Started by the delegate tool (Session#delegate?); a fork has a
|
|
@@ -62,6 +64,8 @@ module Samagotchi
|
|
|
62
64
|
# one read of analytics.json.
|
|
63
65
|
ctx_pct: saved&.ctx_pct&.round(1),
|
|
64
66
|
tokens: saved&.tokens,
|
|
67
|
+
# The memory indexes its prompt held (the card's ctx tooltip).
|
|
68
|
+
memory_index: saved&.memory_index,
|
|
65
69
|
# Hidden from the strip and the list unless "include archived".
|
|
66
70
|
archived: ArchiveStore.archived?(session_dir),
|
|
67
71
|
project_root: session.project_root(cache: root_cache),
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -413,10 +413,9 @@ module Samagotchi
|
|
|
413
413
|
run_engine_turn(nil, continue: true, origin: context_wake.origin,
|
|
414
414
|
id: context_wake.turn_id, max_iterations: IterationLimit.for) do |result, error|
|
|
415
415
|
if error
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
@
|
|
419
|
-
@inbound.unmark_wake(context_wake)
|
|
416
|
+
kept = @turn_flow.prompt_turn_failed(error: error, wake: "the change in attached context #{name}") == :kept
|
|
417
|
+
# A turn that got somewhere stays, its wake mark with it.
|
|
418
|
+
@inbound.unmark_wake(context_wake) unless kept
|
|
420
419
|
@wakes.pause!
|
|
421
420
|
else
|
|
422
421
|
@continue_offer.after_turn(result)
|
|
@@ -508,17 +507,24 @@ module Samagotchi
|
|
|
508
507
|
@wakes.hand_over(reports)
|
|
509
508
|
Log.info(:worker, "delegate_wake_turn", reports: reports.size, in_a_row: @wakes.count!)
|
|
510
509
|
@turn_flow.before_prompt_turn
|
|
510
|
+
restores = @turn_flow.restores
|
|
511
511
|
run_engine_turn(nil, continue: true, origin: reports.first.origin, max_iterations: IterationLimit.for) do |result, error|
|
|
512
512
|
if error
|
|
513
|
-
#
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
513
|
+
# Rolled back, the rings stay (settle_child_reports): the user's
|
|
514
|
+
# next turn takes them. A turn that got somewhere stays, and read them.
|
|
515
|
+
@turn_flow.prompt_turn_failed(
|
|
516
|
+
error: error, wake: reports.size == 1 ? "a delegate's report" : "#{reports.size} delegate reports",
|
|
517
|
+
wake_kept: "chi keeps #{reports.size == 1 ? "the report and brings it" : "the reports and brings them"} " \
|
|
518
|
+
"again with the user's next message."
|
|
519
|
+
)
|
|
519
520
|
@wakes.pause!
|
|
520
521
|
else
|
|
521
522
|
@continue_offer.after_turn(result)
|
|
523
|
+
# Stopped before it got anywhere (a Stop or a before_turn hook's
|
|
524
|
+
# cancel before the model call) and restored: its rings stay, and
|
|
525
|
+
# wait for the user's next message as after a failure, rather
|
|
526
|
+
# than waking again at once.
|
|
527
|
+
@wakes.pause! if result.canceled? && @turn_flow.restores != restores
|
|
522
528
|
end
|
|
523
529
|
end
|
|
524
530
|
true
|
|
@@ -635,6 +641,8 @@ module Samagotchi
|
|
|
635
641
|
model_name: @engine.effective_model_name }
|
|
636
642
|
event[:anytime] = true if anytime
|
|
637
643
|
event[:card] = true if command[:card]
|
|
644
|
+
# /llm-context or /model: what the next turn runs under now (the web's chip).
|
|
645
|
+
event[:llm_context] = @engine.llm_context_summary if changed.intersect?(%i[llm_context model])
|
|
638
646
|
event[:output_truncated] = true if text.length > COMMAND_OUTPUT_LIMIT
|
|
639
647
|
@engine.announce(event)
|
|
640
648
|
end
|
|
@@ -654,6 +662,7 @@ module Samagotchi
|
|
|
654
662
|
# a fresh worker's first), so the list is this turn's from the start.
|
|
655
663
|
@merged_this_turn = []
|
|
656
664
|
@reporting_turn = reports_to_parent?(turn_args[:origin])
|
|
665
|
+
restores = @turn_flow.restores
|
|
657
666
|
begin
|
|
658
667
|
@session.status = Session::STATUS_RUNNING
|
|
659
668
|
@session.save(state_dir: @state_dir)
|
|
@@ -680,15 +689,22 @@ module Samagotchi
|
|
|
680
689
|
SessionInbox.write_output(@session_dir, response)
|
|
681
690
|
end
|
|
682
691
|
save_or_log(:turn) { save_session }
|
|
683
|
-
|
|
692
|
+
# The reports the turn merged were read unless its end took the turn
|
|
693
|
+
# back out of the conversation (TurnFlow restored a checkpoint: a
|
|
694
|
+
# failed prompt or wake turn that got nowhere, a canceled continue
|
|
695
|
+
# turn). A failed turn whose work stayed, and a reminder or continue
|
|
696
|
+
# turn that failed (kept with its failed note), read them.
|
|
697
|
+
settle_child_reports(rolled_back: @turn_flow.restores != restores)
|
|
684
698
|
ring_parent_after_turn
|
|
685
699
|
end
|
|
686
700
|
|
|
687
701
|
# The delegate reports this turn took: a kept turn moves the cursors on
|
|
688
|
-
# and deletes their rings; a
|
|
689
|
-
#
|
|
690
|
-
|
|
691
|
-
|
|
702
|
+
# and deletes their rings; a rolled-back one leaves the rings for the
|
|
703
|
+
# next turn. Once either way: the model reads a report in the
|
|
704
|
+
# conversation, or the next turn brings it (unless a later !rollback
|
|
705
|
+
# erases a kept turn, which drops what it read).
|
|
706
|
+
def settle_child_reports(rolled_back:)
|
|
707
|
+
rolled_back ? @child_reports.release : @child_reports.commit
|
|
692
708
|
end
|
|
693
709
|
|
|
694
710
|
# A delegate child's turn that was its parent's (#reports_to_parent?)
|
|
@@ -735,16 +751,18 @@ module Samagotchi
|
|
|
735
751
|
# prompt it took (its own and any merged into it) goes back to its
|
|
736
752
|
# sender, who can send it again. The rollback and the announcements are
|
|
737
753
|
# one step of the event log: a snapshot shows the failed turn's messages
|
|
738
|
-
# or the restored ones, never the one without the other.
|
|
754
|
+
# or the restored ones, never the one without the other. A turn that
|
|
755
|
+
# got somewhere stays instead (TurnFlow#prompt_turn_failed), with the
|
|
756
|
+
# prompts it took: nothing goes back.
|
|
739
757
|
# @param prompts [Array<Array(String, Hash|nil, Array|nil)>] [prompt,
|
|
740
758
|
# origin, images] (a web client gets its image chips back)
|
|
741
759
|
# @param error [Exception, nil] what failed: its one line stays in the
|
|
742
760
|
# conversation as a turn note
|
|
743
761
|
# Not saved here: run_engine_turn saves after its block.
|
|
744
762
|
def restore_failed_turn(prompts, error: nil)
|
|
745
|
-
note = error && TurnNote.failed(error.respond_to?(:summary) ? error.summary : error.message, restored: true)
|
|
746
763
|
@engine.synchronize_events do
|
|
747
|
-
@turn_flow.prompt_turn_failed(
|
|
764
|
+
next if @turn_flow.prompt_turn_failed(error: error) == :kept
|
|
765
|
+
|
|
748
766
|
prompts.each do |prompt, origin, images|
|
|
749
767
|
restored = { type: :prompt_restored, prompt: prompt, origin: origin }
|
|
750
768
|
restored[:images] = images unless Array(images).empty?
|
|
@@ -73,7 +73,8 @@ module Samagotchi
|
|
|
73
73
|
# A wake turn starts. @return [Integer] the wake turns in a row now
|
|
74
74
|
def count! = @in_a_row += 1
|
|
75
75
|
|
|
76
|
-
# A wake turn failed
|
|
76
|
+
# A wake turn failed (or was stopped before it got anywhere): none
|
|
77
|
+
# starts again until a human's input.
|
|
77
78
|
def pause!
|
|
78
79
|
@paused = true
|
|
79
80
|
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.42.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -312,8 +312,10 @@ files:
|
|
|
312
312
|
- lib/samagotchi/llm/usage.rb
|
|
313
313
|
- lib/samagotchi/llm/utf8_scrub.rb
|
|
314
314
|
- lib/samagotchi/llm_context_apply.rb
|
|
315
|
+
- lib/samagotchi/llm_context_command.rb
|
|
315
316
|
- lib/samagotchi/llm_context_edit.rb
|
|
316
317
|
- lib/samagotchi/llm_context_forget.rb
|
|
318
|
+
- lib/samagotchi/llm_context_override.rb
|
|
317
319
|
- lib/samagotchi/llm_context_stale.rb
|
|
318
320
|
- lib/samagotchi/llm_context_strategy.rb
|
|
319
321
|
- lib/samagotchi/llm_context_view.rb
|
|
@@ -323,6 +325,7 @@ files:
|
|
|
323
325
|
- lib/samagotchi/log_subscriber.rb
|
|
324
326
|
- lib/samagotchi/memory_bundle.rb
|
|
325
327
|
- lib/samagotchi/memory_bundle/builder.rb
|
|
328
|
+
- lib/samagotchi/memory_bundle/index_size.rb
|
|
326
329
|
- lib/samagotchi/memory_bundle/index_sync.rb
|
|
327
330
|
- lib/samagotchi/memory_bundle/index_updater.rb
|
|
328
331
|
- lib/samagotchi/memory_bundle/installer.rb
|
|
@@ -341,6 +344,8 @@ files:
|
|
|
341
344
|
- lib/samagotchi/memory_paths.rb
|
|
342
345
|
- lib/samagotchi/model_catalog.rb
|
|
343
346
|
- lib/samagotchi/model_list_store.rb
|
|
347
|
+
- lib/samagotchi/model_match.rb
|
|
348
|
+
- lib/samagotchi/model_notes.rb
|
|
344
349
|
- lib/samagotchi/model_overlay.rb
|
|
345
350
|
- lib/samagotchi/model_profile.rb
|
|
346
351
|
- lib/samagotchi/model_ref.rb
|
|
@@ -368,6 +373,7 @@ files:
|
|
|
368
373
|
- lib/samagotchi/prompt.rb
|
|
369
374
|
- lib/samagotchi/prompt_history.rb
|
|
370
375
|
- lib/samagotchi/prompt_literal_guard.rb
|
|
376
|
+
- lib/samagotchi/prompt_note.rb
|
|
371
377
|
- lib/samagotchi/prompt_warmup.rb
|
|
372
378
|
- lib/samagotchi/question_desk.rb
|
|
373
379
|
- lib/samagotchi/recap_setup.rb
|
|
@@ -507,6 +513,7 @@ files:
|
|
|
507
513
|
- lib/samagotchi/web/public/init_row.js
|
|
508
514
|
- lib/samagotchi/web/public/live_timing.js
|
|
509
515
|
- lib/samagotchi/web/public/live_turn.js
|
|
516
|
+
- lib/samagotchi/web/public/llm_context_chip.js
|
|
510
517
|
- lib/samagotchi/web/public/model_pick.js
|
|
511
518
|
- lib/samagotchi/web/public/model_picker.js
|
|
512
519
|
- lib/samagotchi/web/public/notifications.js
|