samagotchi 0.31.0 → 0.33.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 +70 -1
- data/README.md +13 -0
- data/bin/chi +3 -0
- data/docs/cli.md +1 -0
- data/docs/configuration.md +3 -1
- data/docs/context.md +220 -0
- data/docs/guardrails.md +12 -4
- data/docs/hooks.md +7 -3
- data/docs/plugins.md +79 -2
- data/docs/releasing.md +3 -2
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +1 -15
- data/lib/samagotchi/bootstrap_command.rb +4 -1
- data/lib/samagotchi/bridge.rb +8 -3
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +22 -0
- data/lib/samagotchi/bundles/github-pr/plugin.rb +65 -0
- data/lib/samagotchi/bundles/github-pr/scripts/pr_context.rb +199 -0
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +15 -2
- data/lib/samagotchi/bundles/guardrails/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +6 -0
- data/lib/samagotchi/child_reports.rb +31 -12
- data/lib/samagotchi/client_id.rb +52 -0
- data/lib/samagotchi/config.rb +6 -0
- data/lib/samagotchi/context_absorber.rb +180 -0
- data/lib/samagotchi/context_command.rb +473 -0
- data/lib/samagotchi/context_fetch.rb +195 -0
- data/lib/samagotchi/context_note.rb +11 -5
- data/lib/samagotchi/context_poller.rb +100 -0
- data/lib/samagotchi/context_providers.rb +159 -0
- data/lib/samagotchi/context_sources.rb +462 -0
- data/lib/samagotchi/engine.rb +19 -11
- data/lib/samagotchi/guardrail_wiring.rb +3 -1
- data/lib/samagotchi/guardrails/parent_approvals.rb +9 -9
- data/lib/samagotchi/guardrails/protected_paths.rb +8 -5
- data/lib/samagotchi/guardrails/shell_lex.rb +0 -9
- data/lib/samagotchi/guardrails/verdict.rb +21 -3
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/memory_bundle/builder.rb +22 -1
- data/lib/samagotchi/memory_bundle/installer.rb +43 -1
- data/lib/samagotchi/memory_bundle/manifest.rb +42 -4
- data/lib/samagotchi/memory_bundle/provenance.rb +17 -1
- data/lib/samagotchi/plugin/attached_context.rb +100 -0
- data/lib/samagotchi/plugin/context.rb +22 -0
- data/lib/samagotchi/plugin/sessions.rb +2 -1
- data/lib/samagotchi/process_group.rb +160 -0
- data/lib/samagotchi/prompt_warmup.rb +20 -1
- data/lib/samagotchi/question_desk.rb +2 -4
- data/lib/samagotchi/relay_verifier.rb +2 -4
- data/lib/samagotchi/reply_wait.rb +9 -4
- data/lib/samagotchi/send_command.rb +2 -1
- data/lib/samagotchi/session.rb +7 -5
- data/lib/samagotchi/session_commands.rb +18 -1
- data/lib/samagotchi/session_manager.rb +7 -6
- data/lib/samagotchi/session_retention.rb +3 -0
- data/lib/samagotchi/steer.rb +36 -19
- data/lib/samagotchi/system_prompt.rb +1 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +12 -7
- data/lib/samagotchi/terminal_ui/formatting.rb +28 -8
- data/lib/samagotchi/terminal_ui.rb +2 -4
- data/lib/samagotchi/tool_activity.rb +8 -0
- data/lib/samagotchi/tool_declarations.rb +21 -0
- data/lib/samagotchi/tools/builtin_calls.rb +1 -2
- data/lib/samagotchi/tools/builtins.rb +5 -0
- data/lib/samagotchi/tools/context_read.rb +163 -0
- data/lib/samagotchi/tools/delegate.rb +2 -2
- data/lib/samagotchi/tools/execute.rb +10 -81
- data/lib/samagotchi/tools/task_runtime.rb +25 -38
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +111 -2
- data/lib/samagotchi/web/public/app.js +57 -28
- data/lib/samagotchi/web/public/composer_size.js +7 -6
- data/lib/samagotchi/web/public/context_chips.js +285 -0
- data/lib/samagotchi/web/public/data.js +24 -0
- data/lib/samagotchi/web/public/index.html +49 -3
- data/lib/samagotchi/web/public/notify.js +5 -3
- data/lib/samagotchi/web/public/question_card.js +3 -4
- data/lib/samagotchi/web/public/shown_texts.js +13 -0
- data/lib/samagotchi/web/public/stable_html.js +18 -0
- data/lib/samagotchi/web/public/timing.js +7 -3
- data/lib/samagotchi/web/public/turn_events.js +16 -3
- data/lib/samagotchi/web/public/turn_view.js +5 -2
- data/lib/samagotchi/worker.rb +154 -6
- metadata +18 -1
|
@@ -472,6 +472,12 @@ body.has-session #history{padding-bottom:calc(var(--dock-h,160px) + 32px)}
|
|
|
472
472
|
#composer .actions-col button{white-space:nowrap;font-size:12px;padding:0 12px;height:28px;min-height:28px;line-height:28px}
|
|
473
473
|
#composer .actions-col .split{order:3;display:flex;gap:4px;margin-left:auto}
|
|
474
474
|
#composer #actionBtn{min-width:64px;font-weight:600}
|
|
475
|
+
/* In a session the composer is one row: the text field (one line, growing with its text; fitPrompt sets its height)
|
|
476
|
+
and Cancel · Send at its bottom right. The key hint is in the placeholder there, so its row goes. */
|
|
477
|
+
body.has-session #composer{flex-direction:row;align-items:flex-end;padding:10px}
|
|
478
|
+
body.has-session #composer textarea{min-height:0}
|
|
479
|
+
body.has-session #composeHint{display:none}
|
|
480
|
+
body.has-session #composer .actions-col{flex-wrap:nowrap;flex-shrink:0}
|
|
475
481
|
/* The model picker (start page only): reads as a label, "host:model ▾", left of Start; a native select under a drawn caret. */
|
|
476
482
|
/* A select is as wide as its widest option: a hidden copy of the chosen name (data-value) sizes the box to that name instead, and the select fills it. */
|
|
477
483
|
#composer .actions-col .model-pick{order:0;position:relative;display:inline-block;max-width:min(50%,380px);height:28px;min-height:28px;padding:0 22px 0 8px;border-radius:8px;overflow:hidden;background:transparent;color:var(--text-muted);font-size:12px;line-height:28px;text-align:left;text-overflow:ellipsis;transition:background .12s ease,color .12s ease}
|
|
@@ -651,7 +657,8 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
651
657
|
#infoBar .meta .copy-attach{padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;background:var(--bg-raised);color:var(--text-2);border:1px solid var(--border-strong)}
|
|
652
658
|
#infoBar .meta .copy-attach:hover{color:var(--text-hover);border-color:var(--border-strong)}
|
|
653
659
|
#infoBar .meta .status{font-size:11px;padding:2px 6px;border-radius:999px}
|
|
654
|
-
|
|
660
|
+
/* The whole path on one line: a long one loses its start ("…/project/dir", the end is what tells dirs apart); the title holds all of it. */
|
|
661
|
+
#infoBar .meta .dir{font-family:ui-monospace,monospace;font-size:11px;color:var(--text-muted);max-width:48ch;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:rtl;text-align:left}
|
|
655
662
|
/* The footer sits on the card's glass: its chips are tinted, not solid, so the glass reads through them. */
|
|
656
663
|
#infoBar .meta .model{font-size:11px;padding:2px 6px;border-radius:6px;background:color-mix(in srgb,var(--bg-hover) 55%,transparent);color:var(--text-3)}
|
|
657
664
|
/* The memory chip ("mem 2", when the session used some): phones only, where the "mem: …" line is hidden. */
|
|
@@ -663,7 +670,7 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
663
670
|
#infoBar .meta .worker-restart{padding:1px 8px;border-radius:5px;font-size:11px;font-weight:600;background:var(--bg-raised);color:var(--warn-bright);border:1px solid var(--warn-border)}
|
|
664
671
|
#infoBar .meta .worker-restart:hover:not(:disabled){background:var(--warn-border);color:var(--on-warn)}
|
|
665
672
|
#infoBar .meta .worker-restart:disabled{opacity:.7;cursor:default}
|
|
666
|
-
#infoBar .preview-line{font-size:13px;word-break:break-word;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:
|
|
673
|
+
#infoBar .preview-line{font-size:13px;word-break:break-word;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;line-clamp:1;overflow:hidden;line-height:1.4;max-height:1.4em}
|
|
667
674
|
#infoBar .preview-line .preview, #infoBar .preview-line .mem{word-break:break-word}
|
|
668
675
|
#infoBar .mem{font-size:12px;color:var(--text-muted)}
|
|
669
676
|
#infoBar button{padding:4px 12px;border-radius:8px;font-size:12px;font-weight:600}
|
|
@@ -673,6 +680,38 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
673
680
|
#infoBar #infoArchiveBtn:hover{color:var(--text);background:var(--bg-hover)}
|
|
674
681
|
#infoBar #infoDeleteBtn{background:transparent;color:var(--text-muted);border:1px solid transparent}
|
|
675
682
|
#infoBar #infoDeleteBtn:hover{color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
|
|
683
|
+
/* Attached context (context_chips.js): a row of its own under the meta; not the composer's #chips/.chip. */
|
|
684
|
+
#contextChips{order:5;flex-basis:100%;display:flex;flex-wrap:wrap;gap:6px;align-items:center;min-width:0}
|
|
685
|
+
#contextChips[hidden]{display:none}
|
|
686
|
+
#contextChips.only-add{flex-basis:auto}
|
|
687
|
+
#infoBar .ctx-chip.ctx-add{border-style:dashed;color:var(--text-muted);background:transparent}
|
|
688
|
+
.ctx-add-form{display:flex;flex-direction:column;gap:6px;margin:0}
|
|
689
|
+
.ctx-add-form .ctx-pop-head strong{font-family:inherit}
|
|
690
|
+
.ctx-add-input{width:100%;box-sizing:border-box;padding:6px 8px;border-radius:7px;border:1px solid var(--border-strong);background:var(--bg-inset);color:var(--text);font:13px/1.4 inherit}
|
|
691
|
+
.ctx-add-input:focus{outline:none;border-color:var(--accent-border)}
|
|
692
|
+
#infoBar .ctx-chip{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:999px;font:600 11px/1.5 ui-monospace,monospace;background:color-mix(in srgb,var(--bg-hover) 55%,transparent);color:var(--text-3);border:1px solid var(--border-strong);white-space:nowrap}
|
|
693
|
+
#infoBar .ctx-chip:hover,#infoBar .ctx-chip.open{color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
694
|
+
#infoBar .ctx-chip.empty{color:var(--text-faint);border-style:dashed}
|
|
695
|
+
#infoBar .ctx-chip.error{color:var(--danger-text);border-color:color-mix(in srgb,var(--danger) 55%,transparent);background:var(--danger-bg-dim)}
|
|
696
|
+
.ctx-dot{width:7px;height:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 2px var(--accent-ring)}
|
|
697
|
+
.ctx-popover{position:fixed;z-index:60;max-height:min(60vh,520px);overflow:auto;padding:12px 14px;border-radius:12px;background:var(--bg-panel);color:var(--text-2);border:1px solid var(--border-strong);box-shadow:var(--toast-shadow);font-size:13px;line-height:1.45;display:flex;flex-direction:column;gap:6px}
|
|
698
|
+
.ctx-popover[hidden]{display:none}
|
|
699
|
+
.ctx-pop-head{display:flex;align-items:baseline;gap:8px}
|
|
700
|
+
.ctx-pop-head strong{font-family:ui-monospace,monospace;color:var(--text-bright)}
|
|
701
|
+
.ctx-pop-kind{font-size:11px;color:var(--text-muted)}
|
|
702
|
+
.ctx-pop-close{margin-left:auto;padding:0 6px;border:none;background:transparent;color:var(--text-muted);font-size:16px;line-height:1}
|
|
703
|
+
.ctx-pop-close:hover{color:var(--text)}
|
|
704
|
+
.ctx-pop-why{color:var(--text)}
|
|
705
|
+
.ctx-pop-hint{font-size:12px;color:var(--accent);word-break:break-all}
|
|
706
|
+
.ctx-pop-summary{padding:6px 8px;border-radius:8px;background:var(--bg-inset);border-left:3px solid var(--accent-border)}
|
|
707
|
+
.ctx-pop-meta{font-size:11px;color:var(--text-muted)}
|
|
708
|
+
.ctx-pop-error{font-size:12px;color:var(--danger-text)}
|
|
709
|
+
.ctx-pop-text{margin:0;max-height:240px;overflow:auto;padding:8px;border-radius:8px;background:var(--bg-inset);font:12px/1.45 ui-monospace,monospace;white-space:pre-wrap;word-break:break-word;color:var(--text-2)}
|
|
710
|
+
.ctx-pop-actions{display:flex;gap:8px;justify-content:flex-end}
|
|
711
|
+
.ctx-pop-actions button{padding:3px 10px;border-radius:7px;font-size:12px;font-weight:600;background:var(--bg-raised);color:var(--text-2);border:1px solid var(--border-strong)}
|
|
712
|
+
.ctx-pop-actions button:hover{color:var(--text-hover)}
|
|
713
|
+
.ctx-pop-actions .ctx-pop-detach{color:var(--text-muted);background:transparent}
|
|
714
|
+
.ctx-pop-actions .ctx-pop-detach:hover{color:var(--danger-text);border-color:color-mix(in srgb,var(--danger) 55%,transparent)}
|
|
676
715
|
/* Phones: the footer is one line; its meta scrolls sideways (all of it stays reachable) and the first-message line, the strip card's title, goes. */
|
|
677
716
|
@media(max-width:600px){
|
|
678
717
|
/* The key hint: cut off at this width, and phones have no Shift+Enter. */
|
|
@@ -688,7 +727,13 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
688
727
|
#infoBar .meta .status{order:-1}
|
|
689
728
|
/* After the status (same order, later in the DOM): the meta scrolls, and only its start is in sight. */
|
|
690
729
|
#infoBar .meta .mem-chip{display:inline;order:-1}
|
|
691
|
-
|
|
730
|
+
/* The chips get a second line of their own (only when there are some), scrolling sideways. */
|
|
731
|
+
#infoBar:has(#contextChips:not([hidden])){flex-wrap:wrap;row-gap:4px}
|
|
732
|
+
#contextChips{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none}
|
|
733
|
+
/* The meta scrolls here: a lone "+ URL" would take its width, so it gets the row too. */
|
|
734
|
+
#contextChips.only-add{flex-basis:100%}
|
|
735
|
+
#contextChips::-webkit-scrollbar{display:none}
|
|
736
|
+
#contextChips>*{flex-shrink:0}
|
|
692
737
|
#infoBar button{padding:4px 8px}
|
|
693
738
|
/* Icon-only buttons (the words stay as the accessible name, the title says more): the meta gets the width. */
|
|
694
739
|
#infoBar>button{font-size:0;line-height:0;padding:5px 7px}
|
|
@@ -892,6 +937,7 @@ a{color:var(--accent)}
|
|
|
892
937
|
</div>
|
|
893
938
|
<div id="infoBar">
|
|
894
939
|
<div class="info" id="infoText"></div>
|
|
940
|
+
<div id="contextChips" aria-label="Attached context" hidden></div>
|
|
895
941
|
<button class="ghost" id="infoArchiveBtn" title="Archive: hide from the lists, keep for good" style="display:none">archive</button>
|
|
896
942
|
<button class="ghost" id="infoStopBtn" title="Stop session (terminate worker)" style="display:none">stop</button>
|
|
897
943
|
<button class="ghost" id="infoDeleteBtn" title="Delete session (for good)" style="display:none">delete</button>
|
|
@@ -13,9 +13,11 @@ export function notifyState({ hasNotification, secureContext, permission, wanted
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// A finished turn shorter than this was watched: no "done". Not a turn
|
|
16
|
-
// chi ran for a delegate child's report (last_turn origin delegate_report)
|
|
17
|
-
// nobody started it, so nobody
|
|
16
|
+
// chi ran for a delegate child's report (last_turn origin delegate_report)
|
|
17
|
+
// or an attached context change (context): nobody started it, so nobody
|
|
18
|
+
// watched it.
|
|
18
19
|
export const DONE_MIN_SECONDS = 10;
|
|
20
|
+
const UNWATCHED_ORIGINS = ["delegate_report", "context"];
|
|
19
21
|
|
|
20
22
|
// The step-limit question (kind "continue": a turn ran out of iterations
|
|
21
23
|
// and asks whether to go on) has its own reason: "out of steps".
|
|
@@ -68,7 +70,7 @@ export function attentionFor(prev, next) {
|
|
|
68
70
|
if (next.parent_id) return null;
|
|
69
71
|
const key = `${next.id}:${turn.ended_at}`;
|
|
70
72
|
if (turn.outcome === "failed") return { reason: "failed", sessionId: next.id, key };
|
|
71
|
-
if (turn.outcome === "completed" && (Number(turn.seconds) >= DONE_MIN_SECONDS || turn.origin
|
|
73
|
+
if (turn.outcome === "completed" && (Number(turn.seconds) >= DONE_MIN_SECONDS || UNWATCHED_ORIGINS.includes(turn.origin))) {
|
|
72
74
|
return { reason: "done", sessionId: next.id, key };
|
|
73
75
|
}
|
|
74
76
|
return null;
|
|
@@ -163,14 +163,13 @@ export function approvalAllowed(pq, answer) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// What the card says when an answer or a dismissal didn't go through. 503
|
|
166
|
-
// (the server found no live bridge) reads as the session not
|
|
167
|
-
// other failure — the worker's own (the server maps it to a 502
|
|
166
|
+
// not_live (the server found no live bridge) reads as the session not
|
|
167
|
+
// running; any other failure — the worker's own (the server maps it to a 502
|
|
168
168
|
// bridge_error) or the connection's — shows what it said, so a worker that
|
|
169
169
|
// failed the request is not mistaken for one that is gone.
|
|
170
170
|
export function answerErrorText(error) {
|
|
171
|
-
const status = error?.status;
|
|
172
171
|
const message = String(error?.message ?? error ?? "");
|
|
173
|
-
if (status === 503 ||
|
|
172
|
+
if (error?.status === 503 || error?.code === "not_live") return "Session is not running — restart it to answer.";
|
|
174
173
|
return message;
|
|
175
174
|
}
|
|
176
175
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The texts the running turn already shows (its generations as they close,
|
|
2
|
+
// the bubbles a render drew): turn_completed's answer gets a bubble of its
|
|
3
|
+
// own only when it isn't one of them (it wasn't streamed). A new turn starts
|
|
4
|
+
// over, so an earlier turn's "Done." doesn't hide this one's.
|
|
5
|
+
export function createShownTexts() {
|
|
6
|
+
let seen = new Set();
|
|
7
|
+
return {
|
|
8
|
+
turnStarted() { seen = new Set(); },
|
|
9
|
+
seed(texts) { seen = new Set(texts.filter(Boolean)); },
|
|
10
|
+
saw(text) { if (text) seen.add(text); },
|
|
11
|
+
shows(text) { return seen.has(text); },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// A block redrawn every tick (the info bar, once a second while a turn
|
|
2
|
+
// runs) keeps its nodes: +html+ is written only when it changed, so a
|
|
3
|
+
// shown tooltip stays up and a press on one of its buttons isn't lost to a
|
|
4
|
+
// rewrite between mousedown and mouseup. +live+ ({selector: text}) names
|
|
5
|
+
// the parts that change every tick (a running clock): +html+ carries them
|
|
6
|
+
// empty and only their text is set, so a tick alone rewrites nothing else.
|
|
7
|
+
const shown = new WeakMap();
|
|
8
|
+
|
|
9
|
+
export function renderStable(el, html, live = {}) {
|
|
10
|
+
if (shown.get(el) !== html) {
|
|
11
|
+
el.innerHTML = html;
|
|
12
|
+
shown.set(el, html);
|
|
13
|
+
}
|
|
14
|
+
for (const [selector, text] of Object.entries(live)) {
|
|
15
|
+
const part = el.querySelector(selector);
|
|
16
|
+
if (part && part.textContent !== text) part.textContent = text;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -112,6 +112,10 @@ export function turnRecordAt(timing, index) {
|
|
|
112
112
|
return Array.isArray(records) ? records[index] || null : null;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// A history item that starts a turn: a prompt, or the note a context wake
|
|
116
|
+
// turn starts with (turn_start).
|
|
117
|
+
const startsTurn = (item) => item?.role === "user" || (item?.role === "note" && item.turn_start === true);
|
|
118
|
+
|
|
115
119
|
// For each history item, the turn index its timing shows under, or null. A
|
|
116
120
|
// turn with tool calls holds several assistant messages; only the last one
|
|
117
121
|
// before the next user message carries the turn's timing. A turn with none
|
|
@@ -122,7 +126,7 @@ export function timedTurnIndexes(items) {
|
|
|
122
126
|
let turnIndex = -1;
|
|
123
127
|
let carrier = null;
|
|
124
128
|
items.forEach((item, i) => {
|
|
125
|
-
if (item
|
|
129
|
+
if (startsTurn(item)) {
|
|
126
130
|
if (carrier !== null) result[carrier] = turnIndex;
|
|
127
131
|
turnIndex += 1;
|
|
128
132
|
carrier = i;
|
|
@@ -150,14 +154,14 @@ export function turnGroups(items, timing) {
|
|
|
150
154
|
let turnIndex = -1;
|
|
151
155
|
let turn = null;
|
|
152
156
|
items.forEach((item, i) => {
|
|
153
|
-
if (item?.role === "note") {
|
|
157
|
+
if (item?.role === "note" && !startsTurn(item)) {
|
|
154
158
|
groups.push({ kind: "note", i });
|
|
155
159
|
} else if (item?.role === "steer") {
|
|
156
160
|
if (turn) turn.steers.push(i);
|
|
157
161
|
else groups.push({ kind: "steer", i });
|
|
158
162
|
} else if (item?.role === "user" && item.merged && turn) {
|
|
159
163
|
turn.merged.push(i);
|
|
160
|
-
} else if (item
|
|
164
|
+
} else if (startsTurn(item)) {
|
|
161
165
|
turnIndex += 1;
|
|
162
166
|
turn = { kind: "turn", turnIndex, turnId: item.turn_id, user: i, texts: [], steers: [], merged: [] };
|
|
163
167
|
groups.push(turn);
|
|
@@ -93,7 +93,13 @@ export function isOwn(clientId, myId) {
|
|
|
93
93
|
|
|
94
94
|
// As the TUI labels them (Formatting::CLIENT_LABELS); this file's label
|
|
95
95
|
// helpers and the TUI's agree per spec/shared/labels_matrix.json.
|
|
96
|
-
|
|
96
|
+
// By the whole id or its prefix (up to and with its first ":"); an id
|
|
97
|
+
// chi doesn't know is "automatic", not the user (ClientId.human?).
|
|
98
|
+
const CLIENT_LABELS = Object.freeze({
|
|
99
|
+
"tui:": "tui", "web:": "web", "system:": "reminder", "delegate:": "delegate", "child:": "delegate report",
|
|
100
|
+
"cli:send": "chi send", "cli:answer": "chi answer", plugin: "plugin",
|
|
101
|
+
});
|
|
102
|
+
const AUTOMATIC_LABEL = "automatic";
|
|
97
103
|
|
|
98
104
|
// A delegate child's report comes in as client child:<id8> (ChildReports).
|
|
99
105
|
const REPORT_CLIENT = "child:";
|
|
@@ -101,10 +107,17 @@ export function isReportClient(clientId) {
|
|
|
101
107
|
return String(clientId || "").startsWith(REPORT_CLIENT);
|
|
102
108
|
}
|
|
103
109
|
|
|
104
|
-
//
|
|
110
|
+
// A turn chi ran because an attached context source changed: context:<name>.
|
|
111
|
+
const CONTEXT_CLIENT = "context:";
|
|
112
|
+
|
|
113
|
+
// The label on another client's prompt, by its client_id prefix; a context
|
|
114
|
+
// change's turn as "context <name> changed".
|
|
105
115
|
export function clientLabel(clientId) {
|
|
106
116
|
if (!clientId) return null;
|
|
107
|
-
|
|
117
|
+
const id = String(clientId);
|
|
118
|
+
if (id.startsWith(CONTEXT_CLIENT) && id.length > CONTEXT_CLIENT.length) return `context ${id.slice(CONTEXT_CLIENT.length)} changed`;
|
|
119
|
+
const colon = id.indexOf(":");
|
|
120
|
+
return CLIENT_LABELS[id] || (colon >= 0 && CLIENT_LABELS[id.slice(0, colon + 1)]) || AUTOMATIC_LABEL;
|
|
108
121
|
}
|
|
109
122
|
|
|
110
123
|
// The note for reminders a turn got (a reminder turn has no prompt).
|
|
@@ -390,7 +390,7 @@ export function createTurnView({
|
|
|
390
390
|
}
|
|
391
391
|
writeClosedLabel(entry, gen);
|
|
392
392
|
if (!entry.toggled) entry.el.open = false;
|
|
393
|
-
// Every generation's text goes into
|
|
393
|
+
// Every generation's text goes into the shown texts (the turn output dedupe).
|
|
394
394
|
const k = normalize(gen.text);
|
|
395
395
|
if (k) sawText(k);
|
|
396
396
|
}
|
|
@@ -826,7 +826,10 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
826
826
|
if (group.kind === "steer") return steerHtml(group.i, true);
|
|
827
827
|
if (group.kind === "answer") return answerHtml(items[group.answer], "");
|
|
828
828
|
const prompt = items[group.user];
|
|
829
|
-
|
|
829
|
+
// A context wake turn starts with its note.
|
|
830
|
+
const parts = [prompt.role === "note"
|
|
831
|
+
? `<div class="bubble note">${noteHtml(prompt)}</div>`
|
|
832
|
+
: `<div class="bubble user"${copySourceAttr(prompt, false)}>${originLabelHtml(prompt)}<div class="user-message">${messageBodyHtml(prompt).body}${thumbs(prompt.images)}</div></div>`];
|
|
830
833
|
const [only] = group.steps;
|
|
831
834
|
const ended = group.answer !== null || group.emptyAnswer != null;
|
|
832
835
|
const textless = only && (only.i === null || !String(items[only.i].content ?? ""));
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "time"
|
|
4
6
|
|
|
7
|
+
require_relative "client_id"
|
|
8
|
+
require_relative "config"
|
|
5
9
|
require_relative "events"
|
|
6
10
|
require_relative "session"
|
|
7
11
|
require_relative "session_inbox"
|
|
@@ -18,6 +22,8 @@ require_relative "iteration_limit"
|
|
|
18
22
|
require_relative "session_commands"
|
|
19
23
|
require_relative "model_profile"
|
|
20
24
|
require_relative "child_reports"
|
|
25
|
+
require_relative "context_absorber"
|
|
26
|
+
require_relative "context_poller"
|
|
21
27
|
require_relative "tools/task_runtime"
|
|
22
28
|
|
|
23
29
|
module Samagotchi
|
|
@@ -51,6 +57,9 @@ module Samagotchi
|
|
|
51
57
|
# gets it over the Bridge a moment after it starts, and that message's
|
|
52
58
|
# turn goes first (the reports join it).
|
|
53
59
|
WAKE_START_GRACE = 2.0
|
|
60
|
+
# One context wake per source in this long (D5): later changes inside
|
|
61
|
+
# it arrive as notes.
|
|
62
|
+
CONTEXT_WAKE_WINDOW = 600
|
|
54
63
|
# How much of a command's output goes into its :command_ran.
|
|
55
64
|
COMMAND_OUTPUT_LIMIT = 4096
|
|
56
65
|
BUSY_OUTPUT = "busy: wait for the turn to end"
|
|
@@ -168,6 +177,9 @@ module Samagotchi
|
|
|
168
177
|
})
|
|
169
178
|
# This session's own delegate children's news (rings in children/).
|
|
170
179
|
@child_reports = ChildReports.new(session_id: @session_id, state_dir: @state_dir)
|
|
180
|
+
# Its attached context's notes (ContextSources).
|
|
181
|
+
@context_absorber = ContextAbsorber.new(session_id: @session_id, state_dir: @state_dir,
|
|
182
|
+
project_root: @session.project_root)
|
|
171
183
|
# /model's default is the config's, as in the REPL; the Engine started
|
|
172
184
|
# on the session's model.
|
|
173
185
|
@default_model = ModelProfile.required_model_name(nil)
|
|
@@ -211,6 +223,11 @@ module Samagotchi
|
|
|
211
223
|
# the background, shown by the UIs; a turn waits only for the ones
|
|
212
224
|
# that bring tools.
|
|
213
225
|
@engine.start_init_tasks!
|
|
226
|
+
# Attached context's commands, in the background; a new text wakes
|
|
227
|
+
# the loop to absorb it. Not activity: the idle exit stops it.
|
|
228
|
+
@context_poller = ContextPoller.new(session_id: @session_id, state_dir: @state_dir,
|
|
229
|
+
project_root: @session.project_root, cwd: @session.working_directory,
|
|
230
|
+
on_change: -> { @waker.wake }).start
|
|
214
231
|
loop do
|
|
215
232
|
# Check if the session was externally marked as stopped. Not
|
|
216
233
|
# stopped_on_disk?, which reads a vanished file as "not stopped":
|
|
@@ -223,7 +240,10 @@ module Samagotchi
|
|
|
223
240
|
next if run_queued_commands
|
|
224
241
|
|
|
225
242
|
# Between turns, so the next turn (the first one too) sees them.
|
|
243
|
+
# A change that asks to wake may start a turn here (C4), but not
|
|
244
|
+
# ahead of an exit or restart a client asked for: it is a note then.
|
|
226
245
|
absorb_notes
|
|
246
|
+
next if absorb_context(wake: nothing_queued? && !@exit_requested)
|
|
227
247
|
|
|
228
248
|
if (prompt = take_initial_prompt)
|
|
229
249
|
run_initial_prompt(prompt) unless initial_command(prompt)
|
|
@@ -263,8 +283,14 @@ module Samagotchi
|
|
|
263
283
|
# step-limit question stays in the file (a save here would write
|
|
264
284
|
# over a stop's status): with no live worker the lists don't read
|
|
265
285
|
# it as waiting, and the next worker drops it (drop_dead_question).
|
|
286
|
+
# The context poller first: a command it runs goes (its process
|
|
287
|
+
# group) before the Engine does.
|
|
288
|
+
@context_poller&.stop
|
|
266
289
|
@engine&.shutdown
|
|
267
290
|
@bridge&.stop
|
|
291
|
+
# The process exits next: a parent wake it rang for (a crash's
|
|
292
|
+
# ring comes just above) must spawn the parent's worker first.
|
|
293
|
+
ChildRing.await_wakes
|
|
268
294
|
end
|
|
269
295
|
end
|
|
270
296
|
|
|
@@ -372,6 +398,123 @@ module Samagotchi
|
|
|
372
398
|
claimed.each { |file| FileUtils.rm_f(file) }
|
|
373
399
|
end
|
|
374
400
|
|
|
401
|
+
# The attached context's notes (ContextAbsorber), between turns. Not
|
|
402
|
+
# into a session with no turn yet (+before_turn+: one is about to
|
|
403
|
+
# run): auto-attached context alone doesn't make a session worth
|
|
404
|
+
# keeping. Saved, then the subscriptions written (a crash between the
|
|
405
|
+
# two re-delivers; the note ids dedupe).
|
|
406
|
+
# +wake+: the session is idle with nothing queued, so an update whose
|
|
407
|
+
# source asked to wake may start a turn (#context_wake_for): its note
|
|
408
|
+
# goes in as the turn's first message.
|
|
409
|
+
# @return [Boolean] whether a wake turn ran
|
|
410
|
+
def absorb_context(before_turn: false, wake: false)
|
|
411
|
+
return false unless before_turn || conversation_started?
|
|
412
|
+
return false if stopped_on_disk?
|
|
413
|
+
|
|
414
|
+
batch = @context_absorber.pending
|
|
415
|
+
return false unless batch
|
|
416
|
+
|
|
417
|
+
waking = wake ? context_wake_for(batch) : nil
|
|
418
|
+
turn_id = SecureRandom.uuid if waking
|
|
419
|
+
batch.deliveries.each do |delivery|
|
|
420
|
+
next unless delivery.note
|
|
421
|
+
|
|
422
|
+
note = delivery == waking ? delivery.wake_note.merge(turn_start: true, turn_id: turn_id) : delivery.note
|
|
423
|
+
@engine.add_context_note(@session, note)
|
|
424
|
+
end
|
|
425
|
+
# Saved even when every note was there already: after a failed save
|
|
426
|
+
# they are in memory only.
|
|
427
|
+
return false if batch.notes.any? && !save_or_log(:context) { @session.save(state_dir: @state_dir) }
|
|
428
|
+
|
|
429
|
+
@context_absorber.commit(waking ? woken(batch, waking) : batch)
|
|
430
|
+
return false unless waking
|
|
431
|
+
|
|
432
|
+
run_context_wake_turn(waking, turn_id)
|
|
433
|
+
true
|
|
434
|
+
rescue SystemCallError, IOError => e
|
|
435
|
+
Log.exception(:worker, "context_failed", e)
|
|
436
|
+
false
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# No input file or queued command waits: the loop would go idle.
|
|
440
|
+
def nothing_queued? = @command_queue.empty? && SessionInbox.find_new_input_files(@session_dir).empty?
|
|
441
|
+
|
|
442
|
+
# The update in +batch+ that may start a wake turn now: its source
|
|
443
|
+
# asked (wake: true), context.wake is on, the shared wake step's
|
|
444
|
+
# conditions hold (no continue offer, wakes not paused by a failed one,
|
|
445
|
+
# under session.max_wakes in a row with no human input, past the start
|
|
446
|
+
# grace) and the source hasn't woken the session in the last
|
|
447
|
+
# CONTEXT_WAKE_WINDOW. The others stay plain notes. Within the start
|
|
448
|
+
# grace a change is a note: it came while the session was away.
|
|
449
|
+
def context_wake_for(batch, now: Time.now)
|
|
450
|
+
candidates = batch.deliveries.select(&:wake_note)
|
|
451
|
+
return nil if candidates.empty? || !context_wakes_on?
|
|
452
|
+
return nil if @wakes_paused || @turn_flow.awaiting_continue? || wake_grace_left.positive?
|
|
453
|
+
|
|
454
|
+
if @wakes_in_a_row >= max_wakes
|
|
455
|
+
Log.info(:worker, "context_wake_held", reason: "max_wakes", names: candidates.map(&:name).join(","))
|
|
456
|
+
return nil
|
|
457
|
+
end
|
|
458
|
+
candidates.find { |delivery| !woke_lately?(delivery.subscription, now) }
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def context_wakes_on? = Config.get("context.wake") != false
|
|
462
|
+
|
|
463
|
+
def woke_lately?(subscription, now)
|
|
464
|
+
at = subscription&.wakes_at && Time.iso8601(subscription.wakes_at)
|
|
465
|
+
at ? now - at < CONTEXT_WAKE_WINDOW : false
|
|
466
|
+
rescue ArgumentError
|
|
467
|
+
false
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# +batch+ with +waking+'s subscription recording the wake (the 10-minute window).
|
|
471
|
+
def woken(batch, waking)
|
|
472
|
+
stamped = waking.with(subscription: waking.subscription.with(wakes_at: Time.now.iso8601))
|
|
473
|
+
batch.with(deliveries: batch.deliveries.map { |delivery| delivery == waking ? stamped : delivery })
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# A turn nobody typed for a changed source (a context wake): a continue
|
|
477
|
+
# turn from context:<name> whose first message is the wake note already
|
|
478
|
+
# in the conversation (named by +turn_id+), on the same rules as a
|
|
479
|
+
# delegate report's wake turn: it counts toward session.max_wakes, and
|
|
480
|
+
# a failure goes back to before the turn (the note stays, as a plain
|
|
481
|
+
# one: #unmark_wake_note) and pauses wakes until a human's input.
|
|
482
|
+
# +waking+: the ContextAbsorber::Delivery that woke it.
|
|
483
|
+
def run_context_wake_turn(waking, turn_id)
|
|
484
|
+
name = waking.name
|
|
485
|
+
@wakes_in_a_row += 1
|
|
486
|
+
Log.info(:worker, "context_wake_turn", name: name, in_a_row: @wakes_in_a_row)
|
|
487
|
+
@turn_flow.before_prompt_turn
|
|
488
|
+
run_engine_turn(nil, continue: true, origin: { client_id: "#{ClientId::CONTEXT_PREFIX}#{name}" },
|
|
489
|
+
id: turn_id, max_iterations: IterationLimit.for) do |result, error|
|
|
490
|
+
if error
|
|
491
|
+
note = TurnNote.failed(error.respond_to?(:summary) ? error.summary : error.message)
|
|
492
|
+
@turn_flow.prompt_turn_failed(note: note)
|
|
493
|
+
unmark_wake_note(turn_id, waking.note)
|
|
494
|
+
@wakes_paused = true
|
|
495
|
+
else
|
|
496
|
+
@continue_offer.after_turn(result)
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# A failed wake turn kept nothing but its note: the update goes back to
|
|
502
|
+
# a plain note (no turn start, the background wording), so a reload
|
|
503
|
+
# draws no empty turn for it.
|
|
504
|
+
def unmark_wake_note(turn_id, note)
|
|
505
|
+
messages = @engine.messages_checkpoint
|
|
506
|
+
index = messages.index { |m| m[:turn_start] && m[:turn_id] == turn_id }
|
|
507
|
+
return unless index
|
|
508
|
+
|
|
509
|
+
messages[index] = ContextNote.message(**note)
|
|
510
|
+
@engine.rollback_to(messages)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# Whether the session has had a turn: a user or assistant message.
|
|
514
|
+
def conversation_started?
|
|
515
|
+
Array(@session.messages).any? { |m| %w[user assistant model].include?((m[:role] || m["role"]).to_s) }
|
|
516
|
+
end
|
|
517
|
+
|
|
375
518
|
# The first prompt (spawn_session's): a delegate child's task, which is
|
|
376
519
|
# its parent's although no client sent it.
|
|
377
520
|
def run_initial_prompt(prompt)
|
|
@@ -400,6 +543,8 @@ module Samagotchi
|
|
|
400
543
|
# its continue turn
|
|
401
544
|
# @param images [Array<Hash>] the prompt's image refs ({file:, name:})
|
|
402
545
|
def run_prompt(prompt, origin, no_interrupt: false, images: [])
|
|
546
|
+
# A first turn sees the context attached before it.
|
|
547
|
+
absorb_context(before_turn: true)
|
|
403
548
|
user_input(origin&.dig(:client_id))
|
|
404
549
|
@continue_offer.drop(origin)
|
|
405
550
|
@turn_flow.before_prompt_turn
|
|
@@ -425,11 +570,11 @@ module Samagotchi
|
|
|
425
570
|
@engine.clear_due_reminder_names!
|
|
426
571
|
return false unless @engine.reminders_due?
|
|
427
572
|
|
|
428
|
-
@continue_offer.drop({ client_id:
|
|
573
|
+
@continue_offer.drop({ client_id: ClientId::REMINDER })
|
|
429
574
|
# A failure has no prompt to hand back (the Engine announced
|
|
430
575
|
# :turn_failed). The offer went before the turn
|
|
431
576
|
# (ContinueOffer#drop); either way the rollback window closes.
|
|
432
|
-
run_engine_turn(nil, continue: true, origin: { client_id:
|
|
577
|
+
run_engine_turn(nil, continue: true, origin: { client_id: ClientId::REMINDER },
|
|
433
578
|
max_iterations: IterationLimit.for) { @turn_flow.after_reminder_turn }
|
|
434
579
|
true
|
|
435
580
|
end
|
|
@@ -633,8 +778,11 @@ module Samagotchi
|
|
|
633
778
|
rescue StandardError => e
|
|
634
779
|
error = e
|
|
635
780
|
# Still running: no turn of the Engine's ended (it never began).
|
|
781
|
+
# It ends here, failed, as an Engine turn would record it: a wait
|
|
782
|
+
# (ReplyWait) tells a turn's end by last_turn.
|
|
636
783
|
if @session.status == Session::STATUS_RUNNING
|
|
637
784
|
@session.status = Session::STATUS_IDLE
|
|
785
|
+
@engine.record_last_turn(@session, "failed", 0, turn_args[:origin], { type: :turn_failed })
|
|
638
786
|
Log.warn(:worker, "turn_not_begun", error: e.class.name, msg: e.message)
|
|
639
787
|
end
|
|
640
788
|
ensure
|
|
@@ -674,12 +822,12 @@ module Samagotchi
|
|
|
674
822
|
# Whether the turn starting now is a delegate child's parent's: its
|
|
675
823
|
# task (the initial prompt), a follow-up, a continue the parent
|
|
676
824
|
# answered, a reminder, anything but a human typing into the child
|
|
677
|
-
# (
|
|
825
|
+
# (ClientId.human?), and any turn after a question the parent
|
|
678
826
|
# was rung about.
|
|
679
827
|
def reports_to_parent?(origin)
|
|
680
828
|
return false unless @session.delegate?
|
|
681
829
|
|
|
682
|
-
@initial_turn || @owes_parent || !
|
|
830
|
+
@initial_turn || @owes_parent || !ClientId.human?(origin&.dig(:client_id))
|
|
683
831
|
end
|
|
684
832
|
|
|
685
833
|
# The Engine published a question (after the question desk saved it).
|
|
@@ -897,11 +1045,11 @@ module Samagotchi
|
|
|
897
1045
|
Log.info(:worker, "idle_exit", idle_s: @idle_exit.idle_seconds.round)
|
|
898
1046
|
end
|
|
899
1047
|
|
|
900
|
-
# A human's input (
|
|
1048
|
+
# A human's input (ClientId.human?) un-archives the session;
|
|
901
1049
|
# a delegate's, a plugin's or a reminder's doesn't.
|
|
902
1050
|
# It also resets the delegate-report wakes (budget and pause).
|
|
903
1051
|
def user_input(client_id)
|
|
904
|
-
return unless
|
|
1052
|
+
return unless ClientId.human?(client_id)
|
|
905
1053
|
|
|
906
1054
|
ArchiveStore.user_input(@session_id, state_dir: @state_dir)
|
|
907
1055
|
@wakes_in_a_row = 0
|
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.33.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -125,6 +125,7 @@ files:
|
|
|
125
125
|
- docs/architecture.md
|
|
126
126
|
- docs/cli.md
|
|
127
127
|
- docs/configuration.md
|
|
128
|
+
- docs/context.md
|
|
128
129
|
- docs/desktop.md
|
|
129
130
|
- docs/guardrails.md
|
|
130
131
|
- docs/hooks.md
|
|
@@ -170,6 +171,9 @@ files:
|
|
|
170
171
|
- lib/samagotchi/bundles/check-in/plugin.rb
|
|
171
172
|
- lib/samagotchi/bundles/core/manifest.yml
|
|
172
173
|
- lib/samagotchi/bundles/dev/manifest.yml
|
|
174
|
+
- lib/samagotchi/bundles/github-pr/manifest.yml
|
|
175
|
+
- lib/samagotchi/bundles/github-pr/plugin.rb
|
|
176
|
+
- lib/samagotchi/bundles/github-pr/scripts/pr_context.rb
|
|
173
177
|
- lib/samagotchi/bundles/guardrails/guardrails.md
|
|
174
178
|
- lib/samagotchi/bundles/guardrails/guardrails/rules.yml
|
|
175
179
|
- lib/samagotchi/bundles/guardrails/guardrails/small-models.yml
|
|
@@ -199,12 +203,19 @@ files:
|
|
|
199
203
|
- lib/samagotchi/cli/flags.rb
|
|
200
204
|
- lib/samagotchi/cli/parent_wait.rb
|
|
201
205
|
- lib/samagotchi/client.rb
|
|
206
|
+
- lib/samagotchi/client_id.rb
|
|
202
207
|
- lib/samagotchi/command_steps.rb
|
|
203
208
|
- lib/samagotchi/commands/registry.rb
|
|
204
209
|
- lib/samagotchi/config.rb
|
|
205
210
|
- lib/samagotchi/config_text_edit.rb
|
|
211
|
+
- lib/samagotchi/context_absorber.rb
|
|
212
|
+
- lib/samagotchi/context_command.rb
|
|
213
|
+
- lib/samagotchi/context_fetch.rb
|
|
206
214
|
- lib/samagotchi/context_note.rb
|
|
215
|
+
- lib/samagotchi/context_poller.rb
|
|
216
|
+
- lib/samagotchi/context_providers.rb
|
|
207
217
|
- lib/samagotchi/context_quote.rb
|
|
218
|
+
- lib/samagotchi/context_sources.rb
|
|
208
219
|
- lib/samagotchi/context_status.rb
|
|
209
220
|
- lib/samagotchi/context_usage.rb
|
|
210
221
|
- lib/samagotchi/context_window.rb
|
|
@@ -315,6 +326,7 @@ files:
|
|
|
315
326
|
- lib/samagotchi/paths.rb
|
|
316
327
|
- lib/samagotchi/pending_input_queue.rb
|
|
317
328
|
- lib/samagotchi/plugin/api.rb
|
|
329
|
+
- lib/samagotchi/plugin/attached_context.rb
|
|
318
330
|
- lib/samagotchi/plugin/context.rb
|
|
319
331
|
- lib/samagotchi/plugin/loader.rb
|
|
320
332
|
- lib/samagotchi/plugin/service.rb
|
|
@@ -323,6 +335,7 @@ files:
|
|
|
323
335
|
- lib/samagotchi/plugin/tool_result.rb
|
|
324
336
|
- lib/samagotchi/plugin_session_state.rb
|
|
325
337
|
- lib/samagotchi/plugin_tasks.rb
|
|
338
|
+
- lib/samagotchi/process_group.rb
|
|
326
339
|
- lib/samagotchi/project_scope.rb
|
|
327
340
|
- lib/samagotchi/prompt.rb
|
|
328
341
|
- lib/samagotchi/prompt_history.rb
|
|
@@ -394,6 +407,7 @@ files:
|
|
|
394
407
|
- lib/samagotchi/tools/builtin_calls.rb
|
|
395
408
|
- lib/samagotchi/tools/builtins.rb
|
|
396
409
|
- lib/samagotchi/tools/cancel_reminder.rb
|
|
410
|
+
- lib/samagotchi/tools/context_read.rb
|
|
397
411
|
- lib/samagotchi/tools/delegate.rb
|
|
398
412
|
- lib/samagotchi/tools/delegate_cursor.rb
|
|
399
413
|
- lib/samagotchi/tools/delegate_relay.rb
|
|
@@ -448,6 +462,7 @@ files:
|
|
|
448
462
|
- lib/samagotchi/web/public/command_list.js
|
|
449
463
|
- lib/samagotchi/web/public/command_view.js
|
|
450
464
|
- lib/samagotchi/web/public/composer_size.js
|
|
465
|
+
- lib/samagotchi/web/public/context_chips.js
|
|
451
466
|
- lib/samagotchi/web/public/copy.js
|
|
452
467
|
- lib/samagotchi/web/public/ctx.js
|
|
453
468
|
- lib/samagotchi/web/public/data.js
|
|
@@ -470,6 +485,8 @@ files:
|
|
|
470
485
|
- lib/samagotchi/web/public/sentences.js
|
|
471
486
|
- lib/samagotchi/web/public/session_state.js
|
|
472
487
|
- lib/samagotchi/web/public/sessions_list.js
|
|
488
|
+
- lib/samagotchi/web/public/shown_texts.js
|
|
489
|
+
- lib/samagotchi/web/public/stable_html.js
|
|
473
490
|
- lib/samagotchi/web/public/stage_model.js
|
|
474
491
|
- lib/samagotchi/web/public/stage_view.js
|
|
475
492
|
- lib/samagotchi/web/public/strip.js
|