samagotchi 0.115.0 → 0.116.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 +53 -1
- data/bin/chi +2 -0
- data/docs/cli.md +5 -1
- data/docs/configuration.md +7 -0
- data/docs/guardrails.md +188 -1
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +4 -0
- data/lib/samagotchi/bridge.rb +26 -0
- data/lib/samagotchi/bridge_client.rb +7 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +15 -0
- data/lib/samagotchi/engine.rb +6 -1
- data/lib/samagotchi/guardrail_wiring.rb +166 -2
- data/lib/samagotchi/guardrails/approval.rb +20 -0
- data/lib/samagotchi/guardrails/targets.rb +23 -2
- data/lib/samagotchi/guardrails/verdict.rb +23 -3
- data/lib/samagotchi/guardrails_command.rb +329 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +13 -0
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_commands.rb +79 -2
- data/lib/samagotchi/session_metrics.rb +7 -1
- data/lib/samagotchi/system_one/answer.rb +78 -0
- data/lib/samagotchi/system_one/approval_question.rb +113 -0
- data/lib/samagotchi/system_one/client.rb +89 -0
- data/lib/samagotchi/system_one/errors.rb +14 -0
- data/lib/samagotchi/system_one/fake_backend.rb +45 -0
- data/lib/samagotchi/system_one/http_backend.rb +129 -0
- data/lib/samagotchi/system_one/log.rb +133 -0
- data/lib/samagotchi/system_one/question.rb +86 -0
- data/lib/samagotchi/system_one/record.rb +83 -0
- data/lib/samagotchi/system_one/self_protect.rb +90 -0
- data/lib/samagotchi/system_one/similar.rb +69 -0
- data/lib/samagotchi/system_one.rb +25 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +15 -4
- data/lib/samagotchi/terminal_ui/event_renderer.rb +1 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +14 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +1 -0
- data/lib/samagotchi/web/public/activity.js +10 -0
- data/lib/samagotchi/web/public/app.js +3 -0
- data/lib/samagotchi/web/public/apple-touch-icon.png +0 -0
- data/lib/samagotchi/web/public/composer_fold.js +8 -8
- data/lib/samagotchi/web/public/composer_grip.js +65 -22
- data/lib/samagotchi/web/public/composer_size.js +30 -2
- data/lib/samagotchi/web/public/data.js +10 -0
- data/lib/samagotchi/web/public/favicon.svg +1 -1
- data/lib/samagotchi/web/public/icons/chi-180.png +0 -0
- data/lib/samagotchi/web/public/icons/chi-192.png +0 -0
- data/lib/samagotchi/web/public/icons/chi-512.png +0 -0
- data/lib/samagotchi/web/public/index.html +5 -0
- data/lib/samagotchi/web/public/system_one_wrong.js +32 -0
- data/lib/samagotchi/web/public/timing.js +9 -5
- data/lib/samagotchi/web/public/turn_events.js +1 -0
- data/lib/samagotchi/web/public/turn_html.js +13 -2
- data/lib/samagotchi/web/session_control.rb +22 -0
- data/lib/samagotchi/web/session_show.rb +35 -5
- metadata +15 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { GRIP_OPEN_PX, gripFloor, gripFolds } from "./composer_size.js";
|
|
1
|
+
import { GRIP_CLICK_FOLD_MS, GRIP_OPEN_PX, gripFloor, gripFolds, gripGesture } from "./composer_size.js";
|
|
2
2
|
import { COMPOSER_FOLD_KEY, composerFolded, foldHandle, opensOnKey } from "./composer_fold.js";
|
|
3
3
|
import { readStored, removeStored, writeStored } from "./storage.js";
|
|
4
4
|
|
|
5
|
-
// The composer's grip and fold (used by app.js): the grip's drag,
|
|
6
|
-
// fold's state per visit and its page-wide listeners. app.js keeps the box's
|
|
5
|
+
// The composer's grip and fold (used by app.js): the grip's drag, click and keys,
|
|
6
|
+
// the fold's state per visit and its page-wide listeners. app.js keeps the box's
|
|
7
7
|
// height (fitPrompt) and the grip's floor (+floor+ / +setFloor+), which
|
|
8
8
|
// fitPrompt reads; the floor is saved under GRIP_KEY.
|
|
9
9
|
export const GRIP_KEY = "chi_composer_floor";
|
|
@@ -24,22 +24,44 @@ export function createComposerGrip({
|
|
|
24
24
|
// past the fold threshold.
|
|
25
25
|
const gripStartTitle = composeGrip.title;
|
|
26
26
|
let gripWillFold = false;
|
|
27
|
+
// A click on the expanded grip folds GRIP_CLICK_FOLD_MS after it, so a
|
|
28
|
+
// second click in that window can still mean "reset the height" (a
|
|
29
|
+
// double-click). A drag or a key acts at once and takes the wait back.
|
|
30
|
+
let pendingFold = null;
|
|
27
31
|
// This visit's state (a visit is one select() call): opened by the page
|
|
28
32
|
// (a focus, a restored line), archived (decided once, from the visit's
|
|
29
33
|
// first getSession answer).
|
|
30
34
|
const newFoldVisit = () => ({ open: false, archived: false, decided: false });
|
|
31
35
|
let foldVisit = newFoldVisit();
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
function cancelPendingFold() {
|
|
38
|
+
if (!pendingFold) return;
|
|
39
|
+
clearTimeout(pendingFold);
|
|
40
|
+
pendingFold = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The grip's custom height, forgotten only by a double-click.
|
|
44
|
+
function resetFloor() {
|
|
45
|
+
setFloor(null);
|
|
46
|
+
removeStored(GRIP_KEY);
|
|
47
|
+
fitPrompt();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// One release of the grip is one gesture, decided by composer_size.js's
|
|
51
|
+
// gripGesture: a drag down past the box folds the composer on release (a
|
|
52
|
+
// session only: the start page's grip only resizes); from folded, a pull up
|
|
53
|
+
// opens it and the box follows the pointer in the same gesture, a tap opens it
|
|
54
|
+
// and focuses the box; a click on the expanded grip folds it too, GRIP_CLICK_FOLD_MS
|
|
55
|
+
// after the release so a second click in that window can still reset the height.
|
|
56
|
+
// No click listener: a click still fires after pointerdown's preventDefault,
|
|
57
|
+
// so the release is the one that acts.
|
|
38
58
|
composeGrip.addEventListener("pointerdown", (e) => {
|
|
39
59
|
e.preventDefault();
|
|
40
60
|
composeGrip.setPointerCapture(e.pointerId);
|
|
41
61
|
composeGrip.classList.add("dragging");
|
|
42
62
|
const tapY = e.clientY;
|
|
63
|
+
// The height the box had before this press: a drag that folds brings it back.
|
|
64
|
+
const floorBefore = floor();
|
|
43
65
|
let startY = tapY;
|
|
44
66
|
let startH = promptEl.offsetHeight;
|
|
45
67
|
let opened = false;
|
|
@@ -76,17 +98,35 @@ export function createComposerGrip({
|
|
|
76
98
|
};
|
|
77
99
|
const up = (ev) => {
|
|
78
100
|
const folds = end();
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
const gesture = gripGesture({
|
|
102
|
+
session: !!session(),
|
|
103
|
+
folded: isComposerFolded(),
|
|
104
|
+
dy: ev.clientY - tapY,
|
|
105
|
+
willFold: folds,
|
|
106
|
+
pending: !!pendingFold,
|
|
107
|
+
});
|
|
108
|
+
if (gesture === "fold") {
|
|
109
|
+
cancelPendingFold();
|
|
110
|
+
// The drag asked for a smaller box, but opening it brings back the
|
|
111
|
+
// height it had before the press: only a double-click forgets it.
|
|
112
|
+
setFloor(floorBefore);
|
|
113
|
+
writeStored(GRIP_KEY, String(floorBefore || ""));
|
|
83
114
|
setComposerFolded(true);
|
|
84
|
-
} else if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
115
|
+
} else if (gesture === "open") {
|
|
116
|
+
cancelPendingFold();
|
|
117
|
+
setComposerFolded(false);
|
|
118
|
+
promptEl.focus();
|
|
119
|
+
} else if (gesture === "reset") {
|
|
120
|
+
resetFloor();
|
|
121
|
+
} else if (gesture === "hide") {
|
|
122
|
+
writeStored(GRIP_KEY, String(floor() || ""));
|
|
123
|
+
pendingFold = setTimeout(() => {
|
|
124
|
+
pendingFold = null;
|
|
125
|
+
setComposerFolded(true);
|
|
126
|
+
}, GRIP_CLICK_FOLD_MS);
|
|
127
|
+
} else if (gesture === "resize") {
|
|
128
|
+
// A drag that stops short of folding: keep the box at the height it asks for.
|
|
129
|
+
cancelPendingFold();
|
|
90
130
|
writeStored(GRIP_KEY, String(floor() || ""));
|
|
91
131
|
// A pull that opened the box leaves the focus in it, as a tap does.
|
|
92
132
|
if (opened) promptEl.focus();
|
|
@@ -103,15 +143,16 @@ export function createComposerGrip({
|
|
|
103
143
|
});
|
|
104
144
|
composeGrip.addEventListener("dblclick", () => {
|
|
105
145
|
if (session() && isComposerFolded()) return;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
fitPrompt();
|
|
146
|
+
cancelPendingFold();
|
|
147
|
+
resetFloor();
|
|
109
148
|
});
|
|
110
149
|
// Enter or Space on the focused handle folds, or opens and focuses the box
|
|
111
|
-
// (a session only; preventDefault: Space would scroll the page).
|
|
150
|
+
// (a session only; preventDefault: Space would scroll the page). At once:
|
|
151
|
+
// the keyboard has no double-click to wait for.
|
|
112
152
|
composeGrip.addEventListener("keydown", (e) => {
|
|
113
153
|
if (!session() || (e.key !== "Enter" && e.key !== " ")) return;
|
|
114
154
|
e.preventDefault();
|
|
155
|
+
cancelPendingFold();
|
|
115
156
|
if (isComposerFolded()) {
|
|
116
157
|
setComposerFolded(false);
|
|
117
158
|
promptEl.focus();
|
|
@@ -135,6 +176,8 @@ export function createComposerGrip({
|
|
|
135
176
|
}
|
|
136
177
|
function applyComposerFolded() {
|
|
137
178
|
const folded = isComposerFolded();
|
|
179
|
+
// Anything else opened the composer: the click's wait for its fold is over.
|
|
180
|
+
if (!folded) cancelPendingFold();
|
|
138
181
|
const was = doc.body.classList.contains("composer-folded");
|
|
139
182
|
doc.body.classList.toggle("composer-folded", folded);
|
|
140
183
|
// Its height was measured while hidden.
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
// The composer's height
|
|
2
|
-
//
|
|
1
|
+
// The composer's height and the gestures of the grip on its top edge. The
|
|
2
|
+
// height grows with the text up to a cap; the grip sets a taller floor (the
|
|
3
|
+
// cap then is at least that floor). A release of the grip is one gesture: a
|
|
4
|
+
// drag that resizes, a drag far enough down that folds the box away, or a
|
|
5
|
+
// click (which, expanded, folds too — after a delay, so a double-click can
|
|
6
|
+
// still reset the height).
|
|
3
7
|
export const PROMPT_MIN_PX = 72; // about three lines of the 15 px field: the start page
|
|
4
8
|
export const PROMPT_LINE_PX = 26; // one line (15 px × 1.45 + 4 padding): in a session
|
|
5
9
|
const PROMPT_CAP_SHARE = 0.35; // of the window's height, for growing with text
|
|
6
10
|
const PROMPT_GRIP_SHARE = 0.5; // of the window's height, for the grip
|
|
7
11
|
export const GRIP_FOLD_PX = 32; // a drag down this far past what the box shows folds the composer (on release)
|
|
8
12
|
export const GRIP_OPEN_PX = 8; // a drag up this far from the folded handle opens it; less is a tap
|
|
13
|
+
export const GRIP_CLICK_FOLD_MS = 250; // how long a click on the expanded grip waits before folding, so a second click in that window can still mean a reset
|
|
9
14
|
|
|
10
15
|
// contentPx: the text's height (scrollHeight plus borders); floorPx: the
|
|
11
16
|
// grip's height, or null; minPx: the empty box's height.
|
|
@@ -28,3 +33,26 @@ export function gripFloor(px, viewportPx, minPx = PROMPT_MIN_PX) {
|
|
|
28
33
|
export function gripFolds({ requestedPx, shownPx }) {
|
|
29
34
|
return shownPx - requestedPx >= GRIP_FOLD_PX;
|
|
30
35
|
}
|
|
36
|
+
|
|
37
|
+
// What a grip release asks for, decided from the press alone (the strip's
|
|
38
|
+
// grip's twin, strip.js's stripGripAction). +session+: is there a session (the
|
|
39
|
+
// start page's grip only resizes). +folded+: the composer is folded now.
|
|
40
|
+
// +dy+: the release y minus the press y, down positive. +willFold+: the drag is
|
|
41
|
+
// past GRIP_FOLD_PX below what the box shows. +pending+: an earlier click's fold
|
|
42
|
+
// is still waiting its GRIP_CLICK_FOLD_MS, so this release is the second click.
|
|
43
|
+
// fold – hide the box now
|
|
44
|
+
// open – a tap on the folded handle
|
|
45
|
+
// hide – the first click on the expanded grip: fold it after the delay
|
|
46
|
+
// reset – the second click in that delay: a double-click, so the height is reset
|
|
47
|
+
// resize – a drag that stops short: keep the box and save the floor it asks for
|
|
48
|
+
// none – a drag down on the folded handle (pulling up opens it while dragging)
|
|
49
|
+
export function gripGesture({ session, folded, dy, willFold, pending }) {
|
|
50
|
+
if (willFold) return "fold";
|
|
51
|
+
const tap = Math.abs(dy) < GRIP_OPEN_PX;
|
|
52
|
+
if (tap) {
|
|
53
|
+
if (folded) return "open";
|
|
54
|
+
if (!session) return "resize";
|
|
55
|
+
return pending ? "reset" : "hide";
|
|
56
|
+
}
|
|
57
|
+
return folded ? "none" : "resize";
|
|
58
|
+
}
|
|
@@ -180,6 +180,16 @@ export function dismissQuestion(id, questionId, opts = {}) {
|
|
|
180
180
|
});
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// Flags a System 1 auto-allow in this session as one it should have asked
|
|
184
|
+
// about; the owner-only route returns 404 for an unknown judgment.
|
|
185
|
+
export function markSystemOneWrong(id, judgmentId, opts = {}) {
|
|
186
|
+
return api(`/api/sessions/${encodeURIComponent(id)}/guardrails/wrong`, {
|
|
187
|
+
method: "POST",
|
|
188
|
+
body: JSON.stringify({ id: judgmentId }),
|
|
189
|
+
...opts,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
183
193
|
export function stopSession(id, opts = {}) {
|
|
184
194
|
return api(`/api/sessions/${id}/stop`, { method: "POST", body: JSON.stringify({}), ...opts });
|
|
185
195
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#1f6feb"/><stop offset="1" stop-color="#6e40c9"/></linearGradient></defs><rect x="1" y="1" width="30" height="30" rx="7.5" fill="url(#g)"/><g fill="none" stroke="#fff" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.8 9.6c2.3-1.3 4-.7 5.2 1.4l6 10.1c1.2 2.1 2.9 2.7 5.2 1.4"/><path d="M22.6 9.2 9.4 22.8"/></g></svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -184,6 +184,11 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
184
184
|
.ts-now button.task-stop{align-self:center}
|
|
185
185
|
.activity-params{color:var(--text-muted);overflow-wrap:anywhere}
|
|
186
186
|
.activity-alias{color:var(--text-faint);white-space:nowrap}
|
|
187
|
+
.activity-auto{color:var(--accent);font-size:11px;white-space:nowrap;cursor:help}
|
|
188
|
+
button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;border:1px solid var(--border-strong);border-radius:6px;background:transparent;color:var(--text-muted);font-size:10px;line-height:16px;white-space:nowrap}
|
|
189
|
+
button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:var(--text)}
|
|
190
|
+
.system-one-wrong.marked{border-color:var(--accent-border);color:var(--text-3)}
|
|
191
|
+
.activity-row.guardrail-wrong .activity-auto{color:var(--text-muted)}
|
|
187
192
|
.activity-duration{color:var(--text-faint);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
188
193
|
.activity-output{margin:4px 0 2px var(--row-gutter);font-size:12px;color:var(--text-muted);white-space:pre-wrap;word-break:break-word;background:var(--bg);border-radius:6px;padding:6px 8px;max-height:160px;overflow:auto}
|
|
189
194
|
.activity-output.tail{display:flex;flex-direction:column-reverse}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { markSystemOneWrong } from "./data.js";
|
|
2
|
+
|
|
3
|
+
// The owner-only correction button on a System 1 auto-allowed tool row.
|
|
4
|
+
export function createSystemOneWrongAction({
|
|
5
|
+
sessionId,
|
|
6
|
+
mark = markSystemOneWrong,
|
|
7
|
+
alert = (text) => globalThis.alert(text),
|
|
8
|
+
}) {
|
|
9
|
+
return async (event) => {
|
|
10
|
+
const button = event.target.closest?.("button.system-one-wrong");
|
|
11
|
+
if (!button) return false;
|
|
12
|
+
|
|
13
|
+
event.preventDefault();
|
|
14
|
+
event.stopPropagation();
|
|
15
|
+
const id = sessionId();
|
|
16
|
+
if (!id || button.disabled) return false;
|
|
17
|
+
|
|
18
|
+
button.disabled = true;
|
|
19
|
+
try {
|
|
20
|
+
await mark(id, button.dataset.judgmentId);
|
|
21
|
+
button.textContent = "should have asked";
|
|
22
|
+
button.setAttribute("aria-pressed", "true");
|
|
23
|
+
button.classList.add("marked");
|
|
24
|
+
button.closest(".activity-row")?.classList.add("guardrail-wrong");
|
|
25
|
+
return true;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
button.disabled = false;
|
|
28
|
+
alert(`Could not mark auto-allow: ${error.message}`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -274,18 +274,22 @@ function canceledSteps(g, record, tools) {
|
|
|
274
274
|
// A call's row from its tool record alone (no saved parts): the record's
|
|
275
275
|
// called_as and via too, so "(called as bash)" and "(via call_tool)" show
|
|
276
276
|
// after a reload, as live, and
|
|
277
|
-
// its no_match ("no match" on a grep that found nothing)
|
|
277
|
+
// its no_match ("no match" on a grep that found nothing) and System 1's
|
|
278
|
+
// auto-allow (guardrail: {rule, p}).
|
|
278
279
|
function recordRows(tools, iteration) {
|
|
279
280
|
return tools.filter((r) => Number(r.iteration) === iteration).map((r) => ({
|
|
280
281
|
key: `${iteration}:${r.call_index ?? 0}`, tool: r.tool, status: r.status, duration_ms: r.duration_ms,
|
|
281
282
|
...(r.called_as ? { called_as: r.called_as } : {}),
|
|
282
283
|
...(r.via ? { via: r.via } : {}),
|
|
283
|
-
...
|
|
284
|
+
...recordFlags(r),
|
|
284
285
|
}));
|
|
285
286
|
}
|
|
286
287
|
|
|
287
|
-
function
|
|
288
|
-
return
|
|
288
|
+
function recordFlags(record) {
|
|
289
|
+
return {
|
|
290
|
+
...(record?.no_match ? { no_match: true } : {}),
|
|
291
|
+
...(record?.guardrail ? { guardrail: record.guardrail } : {}),
|
|
292
|
+
};
|
|
289
293
|
}
|
|
290
294
|
|
|
291
295
|
// A turn whose saved messages carry their parts (the server's ?parts=1):
|
|
@@ -308,7 +312,7 @@ function partsTurn(g, items, record, tools, stepsOnly = false) {
|
|
|
308
312
|
const rows = Array.isArray(parts.tools)
|
|
309
313
|
? parts.tools.map((call, j) => {
|
|
310
314
|
const r = takeRecord(pool, call.tool);
|
|
311
|
-
return { key: `${iteration}:${j + 1}`, status: r?.status || "ok", duration_ms: r?.duration_ms, ...
|
|
315
|
+
return { key: `${iteration}:${j + 1}`, status: r?.status || "ok", duration_ms: r?.duration_ms, ...recordFlags(r), ...call };
|
|
312
316
|
})
|
|
313
317
|
: [];
|
|
314
318
|
const thinking = String(parts.thinking || "");
|
|
@@ -411,6 +411,7 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
|
|
|
411
411
|
};
|
|
412
412
|
// A grep that found nothing: its row stays muted "no match".
|
|
413
413
|
if (part.no_match) completed.activity.no_match = true;
|
|
414
|
+
if (part.guardrail) completed.activity.guardrail = part.guardrail;
|
|
414
415
|
if (part.duration_ms != null) completed.duration_ms = part.duration_ms;
|
|
415
416
|
if (part.diff) completed.diff = part.diff;
|
|
416
417
|
Object.assign(completed, fields);
|
|
@@ -145,7 +145,8 @@ function tailClip(text) {
|
|
|
145
145
|
// part the user toggled (the raw command, the diff, the read lines) stays.
|
|
146
146
|
// +stop+: the live row's stop-task button (a node), after the title.
|
|
147
147
|
export function toolRowTemplate(row, opts = {}) {
|
|
148
|
-
|
|
148
|
+
const wrong = row.guardrail?.should_have_asked ? " guardrail-wrong" : "";
|
|
149
|
+
return html`<div class=${`activity-row${wrong}`} data-key=${row.key}>${toolRowInnerTemplate(row, opts)}</div>`;
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
export function toolRowInnerTemplate(row, { thumbs = () => nothing, stop = nothing } = {}) {
|
|
@@ -163,13 +164,23 @@ export function toolRowInnerTemplate(row, { thumbs = () => nothing, stop = nothi
|
|
|
163
164
|
// tool it was reached through (call_tool), dim.
|
|
164
165
|
const alias = row.called_as ? html`<span class="activity-alias">(called as ${row.called_as})</span>`
|
|
165
166
|
: row.via ? html`<span class="activity-alias">(via ${row.via})</span>` : nothing;
|
|
167
|
+
const auto = row.guardrail
|
|
168
|
+
? html`<span class="activity-auto" title=${`rule ${row.guardrail.rule}, p=${Number(row.guardrail.p).toFixed(2)}, allowed once`}>auto-allowed · System 1</span>`
|
|
169
|
+
: nothing;
|
|
170
|
+
const wrong = row.guardrail?.judgment_id
|
|
171
|
+
? row.guardrail.should_have_asked
|
|
172
|
+
? html`<span class="system-one-wrong marked"
|
|
173
|
+
title="System 1 was told this call should have been asked about">should have asked</span>`
|
|
174
|
+
: html`<button type="button" class="system-one-wrong" data-judgment-id=${row.guardrail.judgment_id}
|
|
175
|
+
title="Tell System 1 this call should have been asked about">should have asked</button>`
|
|
176
|
+
: nothing;
|
|
166
177
|
const duration = formatDuration(row.duration_ms);
|
|
167
178
|
const full = row.status === "running" ? "" : String(row.output || "").replace(/^\[[^\]]*\]\s*/, "");
|
|
168
179
|
const output = full
|
|
169
180
|
? html`<div class=${tailMode(row.tool, full) ? "activity-output tail" : "activity-output"} title=${full}>${inlineOutput(row.tool, full)}</div>`
|
|
170
181
|
: nothing;
|
|
171
182
|
const status = statusParts(row.status, { noMatch: !!row.no_match });
|
|
172
|
-
return html`${statusMarkTemplate(status)}<span class="activity-tool">${toolName(row)}</span>${params}${stop}${alias}${statusWordTemplate(status)}${duration ? html`<span class="activity-duration">${duration}</span>` : nothing}${commandBlockTemplate(row.view, row.title)}${llmEditTemplate(row)}${output}${full ? readLinesTemplate(row) : nothing}${row.images?.length ? thumbs(row.images) : nothing}${row.diff ? activityDiffTemplate(row.diff) : nothing}`;
|
|
183
|
+
return html`${statusMarkTemplate(status)}<span class="activity-tool">${toolName(row)}</span>${params}${stop}${alias}${statusWordTemplate(status)}${auto}${wrong}${duration ? html`<span class="activity-duration">${duration}</span>` : nothing}${commandBlockTemplate(row.view, row.title)}${llmEditTemplate(row)}${output}${full ? readLinesTemplate(row) : nothing}${row.images?.length ? thumbs(row.images) : nothing}${row.diff ? activityDiffTemplate(row.diff) : nothing}`;
|
|
173
184
|
}
|
|
174
185
|
|
|
175
186
|
// A prompt bubble's states and their badges: queued (waiting for its
|
|
@@ -107,6 +107,28 @@ module Samagotchi
|
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
+
# The owner's correction for a System 1 auto-allow. The Bridge checks
|
|
111
|
+
# that the judgment belongs to this worker's session.
|
|
112
|
+
def handle_system_one_wrong(req, id)
|
|
113
|
+
body = parse_json(request_body(req))
|
|
114
|
+
return error_response(400, "invalid_json", "invalid JSON body") unless body.is_a?(Hash)
|
|
115
|
+
|
|
116
|
+
judgment_id = body["id"].to_s.strip
|
|
117
|
+
return error_response(400, "missing_fields", "id is required") if judgment_id.empty?
|
|
118
|
+
|
|
119
|
+
request = ->(client) { client.mark_system_one_wrong(judgment_id: judgment_id) }
|
|
120
|
+
relay(id, bridge_client(id), request, what: "the System 1 judgment was not labelled",
|
|
121
|
+
cant: "label System 1 judgments") do |reply|
|
|
122
|
+
case reply.status
|
|
123
|
+
when 200 then json_response(200, { status: "labelled", id: judgment_id })
|
|
124
|
+
when 404
|
|
125
|
+
error_response(404, "unknown_judgment", "unknown System 1 judgment") if reply.json&.dig("error") == "unknown_judgment"
|
|
126
|
+
when 409 then error_response(409, "already_labelled", "System 1 judgment already has a label")
|
|
127
|
+
when 400 then error_response(400, "invalid_judgment", reply_detail(reply, "invalid System 1 judgment"))
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
110
132
|
# A session command typed in the composer (/model, /models, !rollback,
|
|
111
133
|
# !cmd, /continue): the worker runs it (woken as for a turn) and every
|
|
112
134
|
# UI gets its :command_ran. Needs the worker's Bridge.
|
|
@@ -8,9 +8,11 @@ require_relative "../answer_refs"
|
|
|
8
8
|
require_relative "../answer_tail"
|
|
9
9
|
require_relative "../output_formatter"
|
|
10
10
|
require_relative "../question_asked"
|
|
11
|
+
require_relative "../guardrails/approvals"
|
|
11
12
|
require_relative "../session_commands"
|
|
12
13
|
require_relative "../session_metrics"
|
|
13
14
|
require_relative "../session_setting"
|
|
15
|
+
require_relative "../system_one/log"
|
|
14
16
|
|
|
15
17
|
module Samagotchi
|
|
16
18
|
module Web
|
|
@@ -300,7 +302,7 @@ module Samagotchi
|
|
|
300
302
|
persisted
|
|
301
303
|
end
|
|
302
304
|
turns = Array(source["turn_records"])
|
|
303
|
-
timing_from(source, turns, Array(source["tool_records"]), live: live,
|
|
305
|
+
timing_from(source, turns, Array(source["tool_records"]), session_id: session_id, live: live,
|
|
304
306
|
turn_count: (live && turn_count(live_metrics)) || turns.size)
|
|
305
307
|
end
|
|
306
308
|
|
|
@@ -321,13 +323,13 @@ module Samagotchi
|
|
|
321
323
|
# An older worker's metrics hold every record (before they held the
|
|
322
324
|
# recent ones only, never more than LIVE_RECORDS_CAP turns).
|
|
323
325
|
turns, tools = newest_turn_records(turns, tools, live_metrics["active_turn"]) if turns.size > SessionMetrics::LIVE_RECORDS_CAP
|
|
324
|
-
return timing_from(live_metrics, turns, tools, live: true, turn_count: count)
|
|
326
|
+
return timing_from(live_metrics, turns, tools, session_id: session_id, live: true, turn_count: count)
|
|
325
327
|
end
|
|
326
328
|
|
|
327
329
|
persisted = read_analytics(session_id)
|
|
328
330
|
all = Array(persisted["turn_records"])
|
|
329
331
|
turns, tools = newest_turn_records(all, Array(persisted["tool_records"]), nil)
|
|
330
|
-
timing_from(persisted, turns, tools, live: false, turn_count: all.size)
|
|
332
|
+
timing_from(persisted, turns, tools, session_id: session_id, live: false, turn_count: all.size)
|
|
331
333
|
end
|
|
332
334
|
|
|
333
335
|
# The finished turns a live worker counts: its metrics' turns (the
|
|
@@ -349,7 +351,7 @@ module Samagotchi
|
|
|
349
351
|
[kept, tools.select { |r| r.is_a?(Hash) && ids.include?(r["turn_id"]) }]
|
|
350
352
|
end
|
|
351
353
|
|
|
352
|
-
def timing_from(source, turn_records, tool_records, live:, turn_count:)
|
|
354
|
+
def timing_from(source, turn_records, tool_records, session_id:, live:, turn_count:)
|
|
353
355
|
started_at = source["started_at"]
|
|
354
356
|
last_activity_at = source["last_activity_at"]
|
|
355
357
|
{
|
|
@@ -357,7 +359,7 @@ module Samagotchi
|
|
|
357
359
|
last_activity_at: last_activity_at,
|
|
358
360
|
session_duration_ms: session_duration_ms(started_at, last_activity_at, active: live),
|
|
359
361
|
turn_records: turn_records,
|
|
360
|
-
tool_records: tool_records,
|
|
362
|
+
tool_records: system_one_labelled_records(session_id, tool_records),
|
|
361
363
|
turn_count: turn_count,
|
|
362
364
|
active_turn: source["active_turn"],
|
|
363
365
|
active_tools: Array(source["active_tools"]),
|
|
@@ -376,6 +378,34 @@ module Samagotchi
|
|
|
376
378
|
}
|
|
377
379
|
end
|
|
378
380
|
|
|
381
|
+
# A saved analytics record knows the judgment id, not the later label:
|
|
382
|
+
# the judgments log remains the source of truth across reloads.
|
|
383
|
+
def system_one_labelled_records(session_id, records)
|
|
384
|
+
ids = records.filter_map do |record|
|
|
385
|
+
record.dig("guardrail", "judgment_id") if record.is_a?(Hash) && record["guardrail"].is_a?(Hash)
|
|
386
|
+
end.to_h { |id| [id, true] }
|
|
387
|
+
return records if ids.empty?
|
|
388
|
+
|
|
389
|
+
log = SystemOne::Log.new(dir: Guardrails::Approvals.dir_for(default_state_dir))
|
|
390
|
+
return records unless File.file?(log.path)
|
|
391
|
+
|
|
392
|
+
labelled = log.entries.each_with_object({}) do |entry, result|
|
|
393
|
+
next unless ids[entry["id"]] && entry["session_id"] == session_id && entry["decision"] == "auto" &&
|
|
394
|
+
entry["answer"] == "should_have_asked"
|
|
395
|
+
|
|
396
|
+
result[entry["id"]] = true
|
|
397
|
+
end
|
|
398
|
+
return records if labelled.empty?
|
|
399
|
+
|
|
400
|
+
records.map do |record|
|
|
401
|
+
guardrail = record.is_a?(Hash) && record["guardrail"]
|
|
402
|
+
judgment_id = guardrail.is_a?(Hash) && guardrail["judgment_id"]
|
|
403
|
+
next record unless labelled[judgment_id]
|
|
404
|
+
|
|
405
|
+
record.merge("guardrail" => guardrail.merge("should_have_asked" => true))
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
379
409
|
def read_analytics(session_id)
|
|
380
410
|
AnalyticsFile.read(@session_class.session_dir(session_id, state_dir: default_state_dir)) || {}
|
|
381
411
|
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.116.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -362,6 +362,7 @@ files:
|
|
|
362
362
|
- lib/samagotchi/guardrails/shell_paths.rb
|
|
363
363
|
- lib/samagotchi/guardrails/targets.rb
|
|
364
364
|
- lib/samagotchi/guardrails/verdict.rb
|
|
365
|
+
- lib/samagotchi/guardrails_command.rb
|
|
365
366
|
- lib/samagotchi/hook_question.rb
|
|
366
367
|
- lib/samagotchi/hook_routes.rb
|
|
367
368
|
- lib/samagotchi/hooks.rb
|
|
@@ -576,6 +577,18 @@ files:
|
|
|
576
577
|
- lib/samagotchi/steer.rb
|
|
577
578
|
- lib/samagotchi/steer_cut.rb
|
|
578
579
|
- lib/samagotchi/steer_cut_after.rb
|
|
580
|
+
- lib/samagotchi/system_one.rb
|
|
581
|
+
- lib/samagotchi/system_one/answer.rb
|
|
582
|
+
- lib/samagotchi/system_one/approval_question.rb
|
|
583
|
+
- lib/samagotchi/system_one/client.rb
|
|
584
|
+
- lib/samagotchi/system_one/errors.rb
|
|
585
|
+
- lib/samagotchi/system_one/fake_backend.rb
|
|
586
|
+
- lib/samagotchi/system_one/http_backend.rb
|
|
587
|
+
- lib/samagotchi/system_one/log.rb
|
|
588
|
+
- lib/samagotchi/system_one/question.rb
|
|
589
|
+
- lib/samagotchi/system_one/record.rb
|
|
590
|
+
- lib/samagotchi/system_one/self_protect.rb
|
|
591
|
+
- lib/samagotchi/system_one/similar.rb
|
|
579
592
|
- lib/samagotchi/system_prompt.rb
|
|
580
593
|
- lib/samagotchi/task_finish_note.rb
|
|
581
594
|
- lib/samagotchi/task_watch.rb
|
|
@@ -816,6 +829,7 @@ files:
|
|
|
816
829
|
- lib/samagotchi/web/public/strip.js
|
|
817
830
|
- lib/samagotchi/web/public/strip_grip.js
|
|
818
831
|
- lib/samagotchi/web/public/sw.js
|
|
832
|
+
- lib/samagotchi/web/public/system_one_wrong.js
|
|
819
833
|
- lib/samagotchi/web/public/tally.js
|
|
820
834
|
- lib/samagotchi/web/public/task_stop.js
|
|
821
835
|
- lib/samagotchi/web/public/thinking_chip.js
|