samagotchi 0.147.0 → 0.149.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 +32 -1
- data/README.md +2 -2
- data/docs/architecture.md +1 -1
- data/docs/bundles.md +64 -17
- data/docs/cli.md +18 -0
- data/docs/configuration.md +7 -3
- data/docs/guardrails.md +27 -5
- data/docs/hooks.md +2 -1
- data/docs/internals/objectives.md +264 -0
- data/docs/internals/web-frontend.md +4 -1
- data/docs/plugins.md +4 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +44 -2
- data/docs/system-one.md +18 -2
- data/lib/samagotchi/answer_tail.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/guardrails/merge.yml +21 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +9 -5
- data/lib/samagotchi/bundles/coordinator/plugin.rb +5 -3
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +44 -13
- data/lib/samagotchi/bundles/decision-card/manifest.yml +13 -0
- data/lib/samagotchi/bundles/decision-card/plugin.rb +280 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +12 -1
- data/lib/samagotchi/context_note.rb +17 -5
- data/lib/samagotchi/engine.rb +113 -6
- data/lib/samagotchi/engine_peers.rb +11 -0
- data/lib/samagotchi/guardrail_wiring.rb +11 -6
- data/lib/samagotchi/guardrails/approvals.rb +3 -1
- data/lib/samagotchi/guardrails/pair_boundary.rb +3 -2
- data/lib/samagotchi/guardrails/rules.rb +6 -1
- data/lib/samagotchi/guardrails/verdict.rb +21 -1
- data/lib/samagotchi/guardrails_command.rb +47 -3
- data/lib/samagotchi/llm/chat_loop.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +6 -2
- data/lib/samagotchi/objectives.rb +395 -0
- data/lib/samagotchi/objectives_audit.rb +115 -0
- data/lib/samagotchi/objectives_check.rb +335 -0
- data/lib/samagotchi/objectives_command.rb +192 -0
- data/lib/samagotchi/plugin/api.rb +2 -1
- data/lib/samagotchi/plugin/context.rb +21 -1
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/send_command.rb +2 -1
- data/lib/samagotchi/session_chain.rb +91 -8
- data/lib/samagotchi/session_commands.rb +16 -1
- data/lib/samagotchi/session_continue.rb +6 -2
- data/lib/samagotchi/session_inbox.rb +14 -6
- data/lib/samagotchi/session_manager.rb +1 -1
- data/lib/samagotchi/steer.rb +24 -4
- data/lib/samagotchi/system_one/record.rb +17 -3
- data/lib/samagotchi/testing/boundary.rb +1 -0
- data/lib/samagotchi/testing/host.rb +1 -0
- data/lib/samagotchi/tool_activity.rb +17 -0
- data/lib/samagotchi/tool_declarations.rb +34 -0
- data/lib/samagotchi/tools/builtin_calls.rb +2 -1
- data/lib/samagotchi/tools/builtins.rb +4 -1
- data/lib/samagotchi/tools/objectives_tool.rb +75 -0
- data/lib/samagotchi/tools/peers.rb +3 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/message_display.rb +9 -0
- data/lib/samagotchi/web/participant_view.rb +1 -1
- data/lib/samagotchi/web/public/app.js +10 -0
- data/lib/samagotchi/web/public/cards_place.js +20 -4
- data/lib/samagotchi/web/public/commands.js +4 -1
- data/lib/samagotchi/web/public/index.html +2 -0
- data/lib/samagotchi/web/public/objectives.css +80 -0
- data/lib/samagotchi/web/public/objectives_entry.js +22 -0
- data/lib/samagotchi/web/public/objectives_model.js +100 -0
- data/lib/samagotchi/web/public/objectives_moves.js +145 -0
- data/lib/samagotchi/web/public/objectives_strip.js +242 -0
- data/lib/samagotchi/web/public/objectives_wiring.js +62 -0
- data/lib/samagotchi/web/public/stream_handlers.js +6 -1
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/session_show.rb +12 -1
- data/lib/samagotchi/worker.rb +3 -0
- data/lib/samagotchi/worker_inbound.rb +16 -8
- metadata +16 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// The session's objectives above the conversation: the fold's current state
|
|
2
|
+
// (Objectives.view), from the session read (seed) and live from the stream's
|
|
3
|
+
// objectives_updated (update). No list: the element stays hidden, nothing on
|
|
4
|
+
// the page. A tap opens the edit list (chip_popover.js: a bottom sheet on a
|
|
5
|
+
// phone): add, done, drop, undo and a new text, each a /objectives line on the
|
|
6
|
+
// command route, the one way to an edit, so its refusals are the command's. No
|
|
7
|
+
// message and no turn; the worker's next event redraws (every tab's), and a
|
|
8
|
+
// mid-turn edit shows as waiting until the turn's end. A module of its own:
|
|
9
|
+
// static JS is cached at unversioned URLs.
|
|
10
|
+
import { createChipPopover, SHEET_TALL } from "./chip_popover.js";
|
|
11
|
+
import { addLine, checkText, DONE, DROPPED, editableText, expireChecks, headline, markLine, nextExpiry, notYetText, pendingText, progress, rewordLine, stripItems } from "./objectives_model.js";
|
|
12
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
13
|
+
|
|
14
|
+
const EXPIRY_SLACK_MS = 50;
|
|
15
|
+
const EXPIRY_MAX_MS = 140_000;
|
|
16
|
+
|
|
17
|
+
// @param el the strip's element (index.html #objectivesStrip)
|
|
18
|
+
// @param session () the open session's id
|
|
19
|
+
// @param running () a turn runs
|
|
20
|
+
// @param sendLine (line) posts it as the session's command: a promise
|
|
21
|
+
// @param sendMessage (text) sends it as the user's message: a promise (Not yet)
|
|
22
|
+
// @param failed (error) a refused post, to tell the user
|
|
23
|
+
// @param onChange () the list came or went
|
|
24
|
+
export function createObjectivesStrip({ el, session = () => null, running = () => false, sendLine = async () => {},
|
|
25
|
+
sendMessage = async () => {}, failed = () => {}, onChange = () => {}, doc = document, win = window }) {
|
|
26
|
+
let list = null;
|
|
27
|
+
// The item whose text is being edited; while a post is out.
|
|
28
|
+
let editing = null;
|
|
29
|
+
let busy = false;
|
|
30
|
+
// The ids Not yet was sent for, since the list last changed or the panel closed.
|
|
31
|
+
const told = new Set();
|
|
32
|
+
let expiry = null;
|
|
33
|
+
|
|
34
|
+
// +first+: the one chip a phone's one-line strip keeps (objectives.css).
|
|
35
|
+
function chip(item, first) {
|
|
36
|
+
return html`<button type="button" class=${`obj-chip obj-open ${item.status}${first ? " first" : ""}`} data-id=${item.id} title=${item.text} aria-expanded="false"><span class="obj-id">${item.id}</span><span class="obj-text">${item.status === DONE ? "✓ " : ""}${item.text}</span></button>`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function draw() {
|
|
40
|
+
el.hidden = !list;
|
|
41
|
+
if (!list) {
|
|
42
|
+
render(nothing, el);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const { done, total } = progress(list);
|
|
46
|
+
const waiting = pendingText(list, running());
|
|
47
|
+
render(html`<div class="obj-strip" role="group" aria-label="Objectives">
|
|
48
|
+
<div class="obj-chips">${stripItems(list).map((item) => chip(item, item === headline(list)))}</div>
|
|
49
|
+
${waiting ? html`<span class="obj-waiting" role="status">${waiting}</span>` : nothing}
|
|
50
|
+
<span class="obj-count">${done}/${total}</span>
|
|
51
|
+
<button type="button" class="obj-edit obj-open" aria-expanded="false">Edit</button>
|
|
52
|
+
</div>`, el);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function post(line) {
|
|
56
|
+
if (!line || busy) return;
|
|
57
|
+
busy = true;
|
|
58
|
+
drawPanel();
|
|
59
|
+
Promise.resolve(sendLine(line)).catch(failed).finally(() => {
|
|
60
|
+
busy = false;
|
|
61
|
+
drawPanel();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function subline(item) {
|
|
66
|
+
const parts = [];
|
|
67
|
+
if (item.done_when) parts.push(html`<span>done when: ${item.done_when}</span>`);
|
|
68
|
+
if (item.move) parts.push(html`<span>last move: ${item.move.note}${item.move.evidence ? ` (${item.move.evidence})` : ""}</span>`);
|
|
69
|
+
const check = checkText(item.check);
|
|
70
|
+
if (check) parts.push(html`<span class=${`obj-check ${item.check.verdict || "running"}`} title=${item.check.model ? `Read by ${item.check.model}. Information only.` : "Information only."}>${check}</span>`);
|
|
71
|
+
return parts.length ? html`<div class="obj-row-sub">${parts}</div>` : nothing;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function actions(item) {
|
|
75
|
+
const act = (name, label, title, off = false) => html`<button type="button" class="obj-act ghost" data-obj-act=${name} data-id=${item.id} title=${title} ?disabled=${busy || off}>${label}</button>`;
|
|
76
|
+
// A status button stays apart from it: a check never changes one.
|
|
77
|
+
const check = item.move && item.status !== DROPPED
|
|
78
|
+
? [act("check", "Check this", `One side request reads this move against the session's record. Information only, nothing enters the conversation`, !!item.check?.running)]
|
|
79
|
+
: [];
|
|
80
|
+
if (item.status === "active") {
|
|
81
|
+
const notYet = item.move ? [act("notyet", "Not yet", `Tell the agent: not yet on ${item.id}`, told.has(item.id))] : [];
|
|
82
|
+
return [...check, ...notYet, act("done", "Done", `Mark ${item.id} done`), act("drop", "Drop", `Drop ${item.id}`)];
|
|
83
|
+
}
|
|
84
|
+
return [...check, act("undo", "Undo", `Back to active: ${item.id}`)];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function row(item) {
|
|
88
|
+
const klass = `obj-row ${item.status}`;
|
|
89
|
+
if (editing === item.id) {
|
|
90
|
+
return html`<form class=${klass} data-id=${item.id}>
|
|
91
|
+
<span class="obj-id">${item.id}</span>
|
|
92
|
+
<input class="obj-input" name="text" .value=${editableText(item)} aria-label=${`New text of ${item.id}`} autocomplete="off">
|
|
93
|
+
<button type="submit" class="obj-act" ?disabled=${busy}>Save</button>
|
|
94
|
+
<button type="button" class="obj-act ghost" data-obj-act="cancel">Cancel</button>
|
|
95
|
+
</form>`;
|
|
96
|
+
}
|
|
97
|
+
return html`<div class=${klass} data-row=${item.id}>
|
|
98
|
+
<span class="obj-id">${item.id}</span>
|
|
99
|
+
<div class="obj-row-main">
|
|
100
|
+
<button type="button" class="obj-row-text" data-obj-act="reword" data-id=${item.id} title=${`Edit the text of ${item.id}`}>${item.status === DONE ? "✓ " : ""}${item.text}</button>
|
|
101
|
+
${subline(item)}
|
|
102
|
+
</div>
|
|
103
|
+
<div class="obj-row-acts">${actions(item)}</div>
|
|
104
|
+
</div>`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const panelTemplate = () => {
|
|
108
|
+
const waiting = pendingText(list, running());
|
|
109
|
+
return html`<div class="ctx-pop-head"><span class="ctx-pop-kind">Objectives</span><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div>
|
|
110
|
+
<div class="obj-rows">${list.items.map(row)}</div>
|
|
111
|
+
<form class="obj-add" data-add="1">
|
|
112
|
+
<input class="obj-input" name="text" placeholder="Add an objective" aria-label="Add an objective" autocomplete="off">
|
|
113
|
+
<button type="submit" class="obj-act" ?disabled=${busy}>Add</button>
|
|
114
|
+
</form>
|
|
115
|
+
<div class="obj-note" role="status">${waiting || "No message, no turn: the agent reads the change before its next turn."}</div>`;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const shell = createChipPopover({
|
|
119
|
+
id: "objectivesPanel", className: "ctx-popover obj-panel", width: 560, el, anchor: ".obj-open", summary: () => list,
|
|
120
|
+
session, label: "Objectives", sheet: true, sheetShare: SHEET_TALL, doc, win,
|
|
121
|
+
show: () => render(panelTemplate(), shell.pop),
|
|
122
|
+
onOpen: () => {
|
|
123
|
+
const id = shell.opener?.()?.dataset?.id;
|
|
124
|
+
const target = id && shell.pop.querySelector(`[data-row="${id}"] .obj-row-text`);
|
|
125
|
+
(target || shell.pop.querySelector(".obj-add .obj-input"))?.focus();
|
|
126
|
+
},
|
|
127
|
+
onClose: () => {
|
|
128
|
+
editing = null;
|
|
129
|
+
told.clear();
|
|
130
|
+
},
|
|
131
|
+
onSubmit: (form) => {
|
|
132
|
+
const text = form.elements.text.value;
|
|
133
|
+
if (form.dataset.add) {
|
|
134
|
+
post(addLine(text));
|
|
135
|
+
form.reset();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const line = rewordLine(form.dataset.id, text);
|
|
139
|
+
editing = null;
|
|
140
|
+
// A blank text takes the edit back.
|
|
141
|
+
if (line) post(line);
|
|
142
|
+
else drawPanel();
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
function drawPanel() {
|
|
147
|
+
if (shell.isOpen() && list) render(panelTemplate(), shell.pop);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
shell.pop.addEventListener("click", (e) => {
|
|
151
|
+
const button = e.target.closest("[data-obj-act]");
|
|
152
|
+
if (!button) return;
|
|
153
|
+
const { objAct: act, id } = button.dataset;
|
|
154
|
+
if (act === "reword") {
|
|
155
|
+
editing = id;
|
|
156
|
+
drawPanel();
|
|
157
|
+
const input = shell.pop.querySelector(`form[data-id="${id}"] .obj-input`);
|
|
158
|
+
input?.focus();
|
|
159
|
+
input?.select();
|
|
160
|
+
} else if (act === "cancel") {
|
|
161
|
+
editing = null;
|
|
162
|
+
drawPanel();
|
|
163
|
+
} else if (act === "notyet") {
|
|
164
|
+
notYet(id);
|
|
165
|
+
} else {
|
|
166
|
+
post(markLine(act, id));
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// Esc in the text being edited takes that edit back, not the panel.
|
|
170
|
+
shell.pop.addEventListener("keydown", (e) => {
|
|
171
|
+
if (e.key === "Escape" && editing && e.target.closest("form[data-id]")) {
|
|
172
|
+
e.stopPropagation();
|
|
173
|
+
editing = null;
|
|
174
|
+
drawPanel();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// One tap: a normal message the agent reads at its next step, or as the next
|
|
179
|
+
// turn. Any tab's echo is the stream's.
|
|
180
|
+
// @return {Promise<boolean>} false: it didn't go (a failure, already told)
|
|
181
|
+
async function notYet(id) {
|
|
182
|
+
const item = list?.items.find((i) => i.id === id);
|
|
183
|
+
if (!item || told.has(id)) return false;
|
|
184
|
+
told.add(id);
|
|
185
|
+
drawPanel();
|
|
186
|
+
try {
|
|
187
|
+
await sendMessage(notYetText(item));
|
|
188
|
+
return true;
|
|
189
|
+
} catch (e) {
|
|
190
|
+
told.delete(id);
|
|
191
|
+
failed(e);
|
|
192
|
+
drawPanel();
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// A running check whose worker died gets no event: let it go at its time. The
|
|
198
|
+
// cap keeps a page whose clock is off from holding it for long.
|
|
199
|
+
function armExpiry() {
|
|
200
|
+
win.clearTimeout(expiry);
|
|
201
|
+
const at = nextExpiry(list);
|
|
202
|
+
if (at === null) return;
|
|
203
|
+
expiry = win.setTimeout(() => {
|
|
204
|
+
list = expireChecks(list, Math.max(Date.now(), at));
|
|
205
|
+
draw();
|
|
206
|
+
drawPanel();
|
|
207
|
+
armExpiry();
|
|
208
|
+
}, Math.min(Math.max(at - Date.now(), 0) + EXPIRY_SLACK_MS, EXPIRY_MAX_MS));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// The list without what a check adds: its start and end aren't a change to it.
|
|
212
|
+
const listSig = (view) => JSON.stringify(view?.items?.map(({ check: _check, ...item }) => item) ?? null);
|
|
213
|
+
|
|
214
|
+
const take = (view) => {
|
|
215
|
+
const next = view && Array.isArray(view.items) ? expireChecks(view, Date.now()) : null;
|
|
216
|
+
if (listSig(next) !== listSig(list)) told.clear();
|
|
217
|
+
list = next;
|
|
218
|
+
armExpiry();
|
|
219
|
+
draw();
|
|
220
|
+
shell.refresh();
|
|
221
|
+
onChange();
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
// A session read's list; none clears it.
|
|
226
|
+
seed: take,
|
|
227
|
+
// The stream's objectives_updated.
|
|
228
|
+
update: take,
|
|
229
|
+
// No session open.
|
|
230
|
+
reset: () => {
|
|
231
|
+
if (shell.isOpen()) shell.close();
|
|
232
|
+
take(null);
|
|
233
|
+
},
|
|
234
|
+
notYet,
|
|
235
|
+
hasList: () => !!list,
|
|
236
|
+
// A turn started or ended: the waiting line's words.
|
|
237
|
+
redraw: () => {
|
|
238
|
+
draw();
|
|
239
|
+
drawPanel();
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The objectives' strip and moves, wired to the page's routes, so that app.js
|
|
2
|
+
// holds a few lines of them (its size cap). Every post follows the worker it woke
|
|
3
|
+
// only while the page still shows that session (latest.js), as cards_place.js does.
|
|
4
|
+
// A module of its own: static JS is cached at unversioned URLs.
|
|
5
|
+
import { stillOn } from "./latest.js";
|
|
6
|
+
import { cantText } from "./format.js";
|
|
7
|
+
import { createObjectivesMoves } from "./objectives_moves.js";
|
|
8
|
+
import { createObjectivesStrip } from "./objectives_strip.js";
|
|
9
|
+
|
|
10
|
+
// @param el the strip's element
|
|
11
|
+
// @param historyEl the history
|
|
12
|
+
// @param session () the open session's id
|
|
13
|
+
// @param running () a turn runs
|
|
14
|
+
// @param holding () the stage holds a finished turn (its answer, the newest, isn't in the history yet)
|
|
15
|
+
// @param commands () the session's command listing
|
|
16
|
+
// @param clientId this tab's
|
|
17
|
+
// @param sendCommand, sendTurn data.js's
|
|
18
|
+
// @param follow () follows a worker a post woke
|
|
19
|
+
// @param toast (text, action, options)
|
|
20
|
+
// @return {seed, update, reset, redraw, notYet}
|
|
21
|
+
export function createObjectives({ el, historyEl, session, running, holding = () => false, commands, clientId, sendCommand, sendTurn, follow, toast }) {
|
|
22
|
+
const posted = async (request) => {
|
|
23
|
+
const still = stillOn(session);
|
|
24
|
+
await request(session());
|
|
25
|
+
if (still()) follow();
|
|
26
|
+
};
|
|
27
|
+
const told = (what) => (e) => toast(cantText(what, e), null, { sticky: true });
|
|
28
|
+
|
|
29
|
+
const strip = createObjectivesStrip({
|
|
30
|
+
el, session, running,
|
|
31
|
+
// A tap's edit goes as a card's action does: no echo, the worker woken.
|
|
32
|
+
sendLine: (line) => posted((id) => sendCommand(id, line, { clientId, card: true })),
|
|
33
|
+
// Not yet: a tab of its own as far as the echo goes, so the stream draws it here too.
|
|
34
|
+
sendMessage: (text) => posted((id) => sendTurn(id, text, { clientId: `${clientId}:objectives`, history: false })),
|
|
35
|
+
failed: told("edit the objectives"),
|
|
36
|
+
onChange: () => moves.decorate(),
|
|
37
|
+
});
|
|
38
|
+
const moves = createObjectivesMoves({
|
|
39
|
+
historyEl, notYet: (id) => strip.notYet(id),
|
|
40
|
+
// Not during a turn: its answer isn't there yet, and /short refuses it. Nor
|
|
41
|
+
// with a turn still in the stage: the history's last answer isn't the newest,
|
|
42
|
+
// which is what /short reads. The hand-off fills the history, which redraws this.
|
|
43
|
+
canShorten: () => strip.hasList() && !running() && !holding() && (commands() || []).some((c) => c?.name === "/short"),
|
|
44
|
+
// false: a failure, already told: the button comes back.
|
|
45
|
+
shorten: () => posted((id) => sendCommand(id, "/short", { clientId, card: true })).then(() => true, (e) => {
|
|
46
|
+
told("ask for it")(e);
|
|
47
|
+
return false;
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
seed: strip.seed,
|
|
53
|
+
update: strip.update,
|
|
54
|
+
reset: strip.reset,
|
|
55
|
+
notYet: strip.notYet,
|
|
56
|
+
// A turn started or ended.
|
|
57
|
+
redraw: () => {
|
|
58
|
+
strip.redraw();
|
|
59
|
+
moves.decorate();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -326,6 +326,8 @@ export function createStreamHandlers(deps) {
|
|
|
326
326
|
// one's bubble was drawn at its command_queued: filled in here. One
|
|
327
327
|
// queued for after the turn ran after it: its bubble moves from among
|
|
328
328
|
// the turn's rows to after its answer and timing line.
|
|
329
|
+
// A card's action the worker refused: its buttons come back.
|
|
330
|
+
if (data.card && data.status === "error") deps.cardActionFailed?.(data.line);
|
|
329
331
|
const queued = view.anytime || view.queued ? commandBubble(view.commandId) : null;
|
|
330
332
|
if (view.hidden) queued?.remove();
|
|
331
333
|
else if (queued && view.queued && !view.anytime) {
|
|
@@ -360,10 +362,13 @@ export function createStreamHandlers(deps) {
|
|
|
360
362
|
context_added: (data) => {
|
|
361
363
|
const at = data.at || data.created_at || null;
|
|
362
364
|
gapBefore(at);
|
|
363
|
-
addNoteBubble({ label: data.label, content: data.text || "", at });
|
|
365
|
+
addNoteBubble({ label: data.label, content: data.text || "", at, objectives: data.objectives });
|
|
364
366
|
// An attached context source's note: its chip changed.
|
|
365
367
|
if (data.context_source) contextChips.refresh();
|
|
366
368
|
},
|
|
369
|
+
// The session's list changed (objectives_strip.js): an edit waiting, a
|
|
370
|
+
// list note in, a turn's moves.
|
|
371
|
+
objectives_updated: (data) => deps.objectivesUpdated?.(data),
|
|
367
372
|
// Someone joined, left or was revoked (the worker's ParticipantsWatch):
|
|
368
373
|
// the owner's chip and panel read the list again.
|
|
369
374
|
participants_changed: () => participantsControl?.reload(),
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { toolName } from "./activity.js";
|
|
7
7
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
8
8
|
import { commandBlockTemplate, rowHover } from "./command_view.js";
|
|
9
|
+
import { objectivesEntryTemplate } from "./objectives_entry.js";
|
|
10
|
+
import { moveRowText } from "./objectives_moves.js";
|
|
9
11
|
import { copySource } from "./copy.js";
|
|
10
12
|
import { activityDiffTemplate } from "./diff_view.js";
|
|
11
13
|
import { clip, fullTime, steerSender } from "./format.js";
|
|
@@ -158,7 +160,7 @@ export function toolRowInnerTemplate(row, { thumbs = () => nothing, stop = nothi
|
|
|
158
160
|
const ref = shown ? rowTitleRef(row) : null;
|
|
159
161
|
const attr = (value) => (value ? String(value) : nothing);
|
|
160
162
|
const params = shown
|
|
161
|
-
? html`<span class=${`activity-params${ref?.kind ? " ref" : ""}`} title=${hover || nothing} data-ref-kind=${attr(ref?.kind)} data-ref-path=${attr(ref?.kind && ref.path)} data-ref-line=${attr(ref?.kind && ref.line)} data-ref-end-line=${attr(ref?.kind && ref.end_line)}>${shown}</span>`
|
|
163
|
+
? html`<span class=${`activity-params${ref?.kind ? " ref" : ""}`} title=${hover || nothing} data-ref-kind=${attr(ref?.kind)} data-ref-path=${attr(ref?.kind && ref.path)} data-ref-line=${attr(ref?.kind && ref.line)} data-ref-end-line=${attr(ref?.kind && ref.end_line)}>${moveRowText(row, shown)}</span>`
|
|
162
164
|
: nothing;
|
|
163
165
|
// The name the model called the tool by when it was an alias's, or the
|
|
164
166
|
// tool it was reached through (call_tool), dim.
|
|
@@ -280,6 +282,7 @@ export function answerBubbleTemplate(message, { cls = "", at = null, turnId = nu
|
|
|
280
282
|
// in; the model reads it, nobody asked it anything): who sent it, then its
|
|
281
283
|
// text.
|
|
282
284
|
export function noteBubbleTemplate(note) {
|
|
285
|
+
if (note?.objectives) return objectivesEntryTemplate(note, timeTitle(note.at));
|
|
283
286
|
const from = note?.label ? `note from ${note.label}` : "note";
|
|
284
287
|
return html`<div class="bubble note" title=${timeTitle(note?.at)}><div class="note-line">${from}</div><div class="note-text">${note?.content ?? ""}</div></div>`;
|
|
285
288
|
}
|
|
@@ -6,6 +6,7 @@ require "uri"
|
|
|
6
6
|
require_relative "../analytics_file"
|
|
7
7
|
require_relative "../answer_refs"
|
|
8
8
|
require_relative "../answer_tail"
|
|
9
|
+
require_relative "../objectives"
|
|
9
10
|
require_relative "../output_formatter"
|
|
10
11
|
require_relative "../question_asked"
|
|
11
12
|
require_relative "../guardrails/approvals"
|
|
@@ -91,10 +92,20 @@ module Samagotchi
|
|
|
91
92
|
# The stream cursor `<seq>-<epoch>`: a later worker resets it
|
|
92
93
|
# instead of taking the seq as its own. Only the snapshot has it.
|
|
93
94
|
last_event_id: turn_snapshot && turn_snapshot["event_id"],
|
|
94
|
-
timing: timing
|
|
95
|
+
timing: timing,
|
|
96
|
+
**objectives_field(id, raw_messages)
|
|
95
97
|
})
|
|
96
98
|
end
|
|
97
99
|
|
|
100
|
+
# The session's list (the page's strip), and no key with no list: a
|
|
101
|
+
# session without one reads as it always did. A snapshot's entries have
|
|
102
|
+
# string keys (paired_runs reads symbols).
|
|
103
|
+
def objectives_field(id, messages)
|
|
104
|
+
symbols = Array(messages).map { |message| message.is_a?(Hash) ? message.transform_keys(&:to_sym) : message }
|
|
105
|
+
view = Samagotchi::Objectives.view(id, symbols, state_dir: default_state_dir)
|
|
106
|
+
view ? { objectives: view } : {}
|
|
107
|
+
end
|
|
108
|
+
|
|
98
109
|
# A list in the worker's turn +snapshot+ (queued prompts, commands,
|
|
99
110
|
# init tasks); [] without a worker.
|
|
100
111
|
def snapshot_list(snapshot, key) = Array(snapshot&.[](key))
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -1203,6 +1203,9 @@ module Samagotchi
|
|
|
1203
1203
|
save_or_log(:command) { save_session } unless Array(result.changed).empty?
|
|
1204
1204
|
announce_command(command, status: result.status.to_s, output: result.output, changed: Array(result.changed),
|
|
1205
1205
|
anytime: true, confirm: result.confirm)
|
|
1206
|
+
# It may have left a note (/objectives): an idle loop takes it now, not
|
|
1207
|
+
# at the fallback tick. Harmless otherwise: one more pass.
|
|
1208
|
+
@waker.wake
|
|
1206
1209
|
rescue StandardError => e
|
|
1207
1210
|
Log.warn(:worker, "anytime_command_failed", line: command.line, error: e.class.name, msg: e.message)
|
|
1208
1211
|
end
|
|
@@ -9,6 +9,7 @@ require_relative "client_id"
|
|
|
9
9
|
require_relative "config"
|
|
10
10
|
require_relative "context_note"
|
|
11
11
|
require_relative "log"
|
|
12
|
+
require_relative "objectives"
|
|
12
13
|
require_relative "session"
|
|
13
14
|
require_relative "session_inbox"
|
|
14
15
|
require_relative "steer"
|
|
@@ -113,19 +114,26 @@ module Samagotchi
|
|
|
113
114
|
# note the saved conversation already holds. A failed save keeps the
|
|
114
115
|
# files too: the next pass claims them again, finds them in the
|
|
115
116
|
# conversation and saves again. Not activity: a note alone neither
|
|
116
|
-
# starts a turn nor keeps an idle worker up.
|
|
117
|
+
# starts a turn nor keeps an idle worker up. Of the objectives notes in
|
|
118
|
+
# a batch only the newest goes in: each is a whole list, so the older
|
|
119
|
+
# ones (edits made during a turn) would only be sent for nothing.
|
|
117
120
|
def absorb_notes
|
|
118
121
|
files = SessionInbox.find_new_note_files(@session_dir)
|
|
119
122
|
return if files.empty? || @stopped.call
|
|
120
123
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
# Under the objectives' lock: an edit reads the conversation and then the
|
|
125
|
+
# pending file, so it must see this whole step or none of it.
|
|
126
|
+
Objectives.with_lock(@session_dir) do
|
|
127
|
+
claimed = files.filter_map { |file| SessionInbox.claim_note_file(file) }
|
|
128
|
+
notes = claimed.filter_map { |file| SessionInbox.read_note(file) }
|
|
129
|
+
newest_list = notes.reverse.find { |note| note[:objectives] }
|
|
130
|
+
notes.each do |note|
|
|
131
|
+
@engine.add_context_note(@session, note) if !note[:objectives] || note.equal?(newest_list)
|
|
132
|
+
end
|
|
133
|
+
next unless save(:notes)
|
|
134
|
+
|
|
135
|
+
claimed.each { |file| FileUtils.rm_f(file) }
|
|
125
136
|
end
|
|
126
|
-
return unless save(:notes)
|
|
127
|
-
|
|
128
|
-
claimed.each { |file| FileUtils.rm_f(file) }
|
|
129
137
|
end
|
|
130
138
|
|
|
131
139
|
# The attached context's notes (ContextAbsorber), between turns. Not
|
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.149.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -163,6 +163,7 @@ files:
|
|
|
163
163
|
- docs/internals/llm-context-bench.md
|
|
164
164
|
- docs/internals/llm-context-forget.md
|
|
165
165
|
- docs/internals/llm-context-live.md
|
|
166
|
+
- docs/internals/objectives.md
|
|
166
167
|
- docs/internals/participants.md
|
|
167
168
|
- docs/internals/prompt-caching.md
|
|
168
169
|
- docs/internals/remote-sessions.md
|
|
@@ -241,12 +242,15 @@ files:
|
|
|
241
242
|
- lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md
|
|
242
243
|
- lib/samagotchi/bundles/codex-peer/scripts/pair
|
|
243
244
|
- lib/samagotchi/bundles/codex-peer/skill_codex_peer.md
|
|
245
|
+
- lib/samagotchi/bundles/coordinator/guardrails/merge.yml
|
|
244
246
|
- lib/samagotchi/bundles/coordinator/manifest.yml
|
|
245
247
|
- lib/samagotchi/bundles/coordinator/plugin.rb
|
|
246
248
|
- lib/samagotchi/bundles/coordinator/skill_coordinator.md
|
|
247
249
|
- lib/samagotchi/bundles/core/manifest.yml
|
|
248
250
|
- lib/samagotchi/bundles/cost-cap/manifest.yml
|
|
249
251
|
- lib/samagotchi/bundles/cost-cap/plugin.rb
|
|
252
|
+
- lib/samagotchi/bundles/decision-card/manifest.yml
|
|
253
|
+
- lib/samagotchi/bundles/decision-card/plugin.rb
|
|
250
254
|
- lib/samagotchi/bundles/dev/manifest.yml
|
|
251
255
|
- lib/samagotchi/bundles/github-pr/manifest.yml
|
|
252
256
|
- lib/samagotchi/bundles/github-pr/plugin.rb
|
|
@@ -504,6 +508,10 @@ files:
|
|
|
504
508
|
- lib/samagotchi/muted_memories.rb
|
|
505
509
|
- lib/samagotchi/note_command.rb
|
|
506
510
|
- lib/samagotchi/note_delivery.rb
|
|
511
|
+
- lib/samagotchi/objectives.rb
|
|
512
|
+
- lib/samagotchi/objectives_audit.rb
|
|
513
|
+
- lib/samagotchi/objectives_check.rb
|
|
514
|
+
- lib/samagotchi/objectives_command.rb
|
|
507
515
|
- lib/samagotchi/output_formatter.rb
|
|
508
516
|
- lib/samagotchi/owner_lock.rb
|
|
509
517
|
- lib/samagotchi/owner_prompt.rb
|
|
@@ -742,6 +750,7 @@ files:
|
|
|
742
750
|
- lib/samagotchi/tools/list_reminders.rb
|
|
743
751
|
- lib/samagotchi/tools/list_sessions.rb
|
|
744
752
|
- lib/samagotchi/tools/memory.rb
|
|
753
|
+
- lib/samagotchi/tools/objectives_tool.rb
|
|
745
754
|
- lib/samagotchi/tools/output_guardrails.rb
|
|
746
755
|
- lib/samagotchi/tools/peers.rb
|
|
747
756
|
- lib/samagotchi/tools/read.rb
|
|
@@ -871,6 +880,12 @@ files:
|
|
|
871
880
|
- lib/samagotchi/web/public/notice_links.js
|
|
872
881
|
- lib/samagotchi/web/public/notifications.js
|
|
873
882
|
- lib/samagotchi/web/public/notify.js
|
|
883
|
+
- lib/samagotchi/web/public/objectives.css
|
|
884
|
+
- lib/samagotchi/web/public/objectives_entry.js
|
|
885
|
+
- lib/samagotchi/web/public/objectives_model.js
|
|
886
|
+
- lib/samagotchi/web/public/objectives_moves.js
|
|
887
|
+
- lib/samagotchi/web/public/objectives_strip.js
|
|
888
|
+
- lib/samagotchi/web/public/objectives_wiring.js
|
|
874
889
|
- lib/samagotchi/web/public/participant.html
|
|
875
890
|
- lib/samagotchi/web/public/participant.js
|
|
876
891
|
- lib/samagotchi/web/public/participant_annotate.js
|