samagotchi 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -1
- data/README.md +1 -1
- data/bin/chi +19 -2
- data/docs/cli.md +32 -7
- data/docs/configuration.md +34 -3
- data/docs/guardrails.md +15 -3
- data/docs/internals/prompt-caching.md +2 -1
- data/docs/memory.md +108 -1
- data/docs/sessions.md +5 -5
- data/docs/testing.md +16 -0
- data/lib/samagotchi/bridge/card_store.rb +6 -7
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +23 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +5 -5
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +13 -0
- data/lib/samagotchi/bundles/system/self_map.md +4 -2
- data/lib/samagotchi/child_reports.rb +8 -4
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_command.rb +5 -2
- data/lib/samagotchi/engine.rb +121 -16
- data/lib/samagotchi/events.rb +8 -1
- data/lib/samagotchi/guardrails/model_size.rb +3 -4
- data/lib/samagotchi/guardrails/rules.rb +27 -17
- data/lib/samagotchi/guardrails/targets.rb +40 -1
- data/lib/samagotchi/guardrails/verdict.rb +6 -3
- data/lib/samagotchi/llm/errors.rb +33 -1
- data/lib/samagotchi/llm_context_command.rb +151 -0
- data/lib/samagotchi/llm_context_override.rb +180 -0
- data/lib/samagotchi/llm_context_strategy.rb +63 -13
- data/lib/samagotchi/memory_bundle/index_size.rb +103 -0
- data/lib/samagotchi/memory_bundle/index_sync.rb +3 -2
- data/lib/samagotchi/model_match.rb +54 -0
- data/lib/samagotchi/model_notes.rb +205 -0
- data/lib/samagotchi/parent_report.rb +12 -2
- data/lib/samagotchi/plugin/context.rb +3 -2
- data/lib/samagotchi/plugin/sessions.rb +3 -1
- data/lib/samagotchi/prompt_note.rb +53 -0
- data/lib/samagotchi/reply_wait.rb +3 -17
- data/lib/samagotchi/self_report.rb +47 -0
- data/lib/samagotchi/send_command.rb +22 -2
- data/lib/samagotchi/session.rb +44 -4
- data/lib/samagotchi/session_commands.rb +45 -6
- data/lib/samagotchi/session_manager.rb +5 -2
- data/lib/samagotchi/session_metrics.rb +47 -7
- data/lib/samagotchi/system_prompt.rb +79 -11
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +13 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +32 -12
- data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +40 -0
- data/lib/samagotchi/terminal_ui.rb +16 -6
- data/lib/samagotchi/tool_runner.rb +26 -1
- data/lib/samagotchi/tools/memory.rb +70 -3
- data/lib/samagotchi/turn_flow.rb +44 -8
- data/lib/samagotchi/turn_note.rb +40 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +35 -6
- data/lib/samagotchi/web/public/app.js +47 -7
- data/lib/samagotchi/web/public/ctx.js +28 -5
- data/lib/samagotchi/web/public/format.js +29 -2
- data/lib/samagotchi/web/public/index.html +12 -0
- data/lib/samagotchi/web/public/live_turn.js +1 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +194 -0
- data/lib/samagotchi/web/public/session_state.js +6 -0
- data/lib/samagotchi/web/public/timing.js +8 -2
- data/lib/samagotchi/web/public/turn_view.js +15 -2
- data/lib/samagotchi/web/session_summary.rb +4 -0
- data/lib/samagotchi/worker.rb +36 -18
- data/lib/samagotchi/worker_wakes.rb +2 -1
- metadata +8 -1
data/lib/samagotchi/turn_flow.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "output_formatter"
|
|
4
4
|
require_relative "turn_note"
|
|
5
5
|
require_relative "context_note"
|
|
6
|
+
require_relative "llm/errors"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
# The state around turns that the REPL and a session worker share, kept out
|
|
@@ -45,11 +46,18 @@ module Samagotchi
|
|
|
45
46
|
# (context summarizes the interrupted turn for a "no, <reason>")
|
|
46
47
|
attr_reader :offer
|
|
47
48
|
|
|
49
|
+
# How many times the conversation went back to a checkpoint (a failed
|
|
50
|
+
# or canceled turn's restore, !rollback): a worker compares it across a
|
|
51
|
+
# turn to tell whether the turn's messages (the delegate reports it
|
|
52
|
+
# merged) left the conversation.
|
|
53
|
+
attr_reader :restores
|
|
54
|
+
|
|
48
55
|
def initialize(engine:)
|
|
49
56
|
@engine = engine
|
|
50
57
|
@checkpoint = nil
|
|
51
58
|
@continue_checkpoint = nil
|
|
52
59
|
@offer = nil
|
|
60
|
+
@restores = 0
|
|
53
61
|
end
|
|
54
62
|
|
|
55
63
|
def awaiting_continue? = !@offer.nil?
|
|
@@ -95,17 +103,44 @@ module Samagotchi
|
|
|
95
103
|
end
|
|
96
104
|
end
|
|
97
105
|
|
|
98
|
-
# A prompt turn
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
|
|
106
|
+
# A prompt turn (or a wake turn) failed. One that got somewhere (the
|
|
107
|
+
# Engine kept its tool results: LLM::FailedTurn.kept_steps, never for
|
|
108
|
+
# a context overflow) stays, as after a cancel: its tool calls ran, and
|
|
109
|
+
# the model must see what they did. The checkpoint stays for !rollback, and
|
|
110
|
+
# the prompt is answered by what the turn did, so it doesn't go back to
|
|
111
|
+
# its sender. One that got nowhere goes back to the conversation before
|
|
112
|
+
# it, and its prompt to its sender (the caller hands it back).
|
|
113
|
+
# Either way the note at the tail says why (TurnNote.failed), in place
|
|
114
|
+
# of an older one, so the model reads why its last message went
|
|
115
|
+
# unanswered.
|
|
116
|
+
# @param error [Exception, nil] what failed: its one line is the note's
|
|
117
|
+
# @param wake [String, nil] what woke the turn (TurnNote.failed's wake:)
|
|
118
|
+
# @param wake_kept [String, nil] what becomes of the wake's news when
|
|
119
|
+
# the turn is rolled back (TurnNote.failed's kept:)
|
|
120
|
+
# @param note [Boolean] false: no note (an image that never reached the
|
|
121
|
+
# model)
|
|
122
|
+
# @return [Symbol] :kept or :restored
|
|
123
|
+
def prompt_turn_failed(error: nil, wake: nil, wake_kept: nil, note: true)
|
|
124
|
+
summary = error && (error.respond_to?(:summary) ? error.summary : error.message)
|
|
125
|
+
steps = LLM::FailedTurn.kept_steps(error)
|
|
126
|
+
@offer = nil
|
|
127
|
+
if steps
|
|
128
|
+
# The Engine's note says so already; a wake turn's says what woke it.
|
|
129
|
+
if wake && note
|
|
130
|
+
@engine.rollback_to(TurnNote.replace_trailing(@engine.messages_checkpoint,
|
|
131
|
+
TurnNote.failed(summary, wake: wake, steps: steps)))
|
|
132
|
+
end
|
|
133
|
+
return :kept
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
failed = note && summary && TurnNote.failed(summary, restored: wake.nil?, wake: wake, kept: wake_kept)
|
|
102
137
|
if @checkpoint
|
|
103
|
-
restore(@checkpoint, note:
|
|
104
|
-
elsif
|
|
105
|
-
@engine.rollback_to(TurnNote.replace_trailing(@engine.messages_checkpoint,
|
|
138
|
+
restore(@checkpoint, note: failed)
|
|
139
|
+
elsif failed
|
|
140
|
+
@engine.rollback_to(TurnNote.replace_trailing(@engine.messages_checkpoint, failed))
|
|
106
141
|
end
|
|
107
|
-
@offer = nil
|
|
108
142
|
@checkpoint = nil
|
|
143
|
+
:restored
|
|
109
144
|
end
|
|
110
145
|
|
|
111
146
|
# Answer the offer with no (Stop): the interrupted turn stays, as
|
|
@@ -170,6 +205,7 @@ module Samagotchi
|
|
|
170
205
|
restored = Array(checkpoint) + arrived
|
|
171
206
|
restored = TurnNote.replace_trailing(restored, note) if note
|
|
172
207
|
@engine.rollback_to(restored)
|
|
208
|
+
@restores += 1
|
|
173
209
|
end
|
|
174
210
|
|
|
175
211
|
def conversation_of(result)
|
data/lib/samagotchi/turn_note.rb
CHANGED
|
@@ -17,6 +17,10 @@ module Samagotchi
|
|
|
17
17
|
OPEN = "[SYSTEM: "
|
|
18
18
|
CLOSE = "]"
|
|
19
19
|
FAILED = "the previous turn failed before any answer: "
|
|
20
|
+
# A failed turn whose work stayed (#failed_after): "failed after N tool
|
|
21
|
+
# steps: <why>. Its work so far … stays; …".
|
|
22
|
+
FAILED_AFTER = "the previous turn failed after "
|
|
23
|
+
KEPT_WORK = ". Its work so far (tool calls, file changes) stays; "
|
|
20
24
|
RESTORED = "The message went back to the user, who may send it again."
|
|
21
25
|
TASKS_LISTED = 5
|
|
22
26
|
TASK_COMMAND_CHARS = 60
|
|
@@ -41,7 +45,12 @@ module Samagotchi
|
|
|
41
45
|
# wrote nothing, and wakes pause until they do
|
|
42
46
|
# @param kept [String, nil] what becomes of the wake's news, after
|
|
43
47
|
# that (a delegate report's rings stay for the user's next turn)
|
|
44
|
-
|
|
48
|
+
# @param steps [Integer, nil] the turn's work stays in the conversation
|
|
49
|
+
# (LLM::FailedTurn.progress): the tool results it got to. Not with
|
|
50
|
+
# +restored+, which took the turn out.
|
|
51
|
+
def failed(summary, restored: false, continued: false, wake: nil, kept: nil, steps: nil)
|
|
52
|
+
return failed_after(summary, steps, continued: continued, wake: wake) if steps && !restored
|
|
53
|
+
|
|
45
54
|
tail = if restored then RESTORED
|
|
46
55
|
elsif wake
|
|
47
56
|
"The wake turn for #{one_line(wake)} was not answered; chi starts no other wake turn until the user writes." +
|
|
@@ -52,6 +61,36 @@ module Samagotchi
|
|
|
52
61
|
message("#{FAILED}#{one_line(summary)}. #{tail}")
|
|
53
62
|
end
|
|
54
63
|
|
|
64
|
+
# A turn that failed partway, its work kept: "the previous turn failed
|
|
65
|
+
# after 3 tool steps: <why>. Its work so far (tool calls, file changes)
|
|
66
|
+
# stays; …".
|
|
67
|
+
def failed_after(summary, steps, continued: false, wake: nil)
|
|
68
|
+
count = "#{steps} tool step#{"s" if steps != 1}"
|
|
69
|
+
tail = if wake
|
|
70
|
+
"the wake turn for #{one_line(wake)} stopped there; chi starts no other wake turn until the user writes."
|
|
71
|
+
elsif continued then "the continued turn stopped there."
|
|
72
|
+
else
|
|
73
|
+
"the user's last message is not answered yet."
|
|
74
|
+
end
|
|
75
|
+
message("#{FAILED_AFTER}#{count}: #{one_line(summary)}#{KEPT_WORK}#{tail}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The failure summary of the failed-turn note at the tail of +messages+
|
|
79
|
+
# (behind context notes at most), either kind (rolled back, or its work
|
|
80
|
+
# kept), or nil without one.
|
|
81
|
+
def failure_summary(messages)
|
|
82
|
+
list = Array(messages)
|
|
83
|
+
index = trailing_index(list)
|
|
84
|
+
return nil unless index
|
|
85
|
+
|
|
86
|
+
text = (list[index][:content] || list[index]["content"]).to_s
|
|
87
|
+
if text.start_with?("#{OPEN}#{FAILED}")
|
|
88
|
+
text["#{OPEN}#{FAILED}".length..].sub(/\. [^.]*\.\]\z/, "")
|
|
89
|
+
elsif text.start_with?("#{OPEN}#{FAILED_AFTER}") && text.include?(KEPT_WORK)
|
|
90
|
+
text[0...text.rindex(KEPT_WORK)].sub(/\A#{Regexp.escape(OPEN + FAILED_AFTER)}\d+ tool steps?: /, "")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
55
94
|
# @param reason [Symbol, String, nil] the cancel reason (:ctrl_c, :user…)
|
|
56
95
|
# @param seconds [Numeric, nil] how long the turn had run
|
|
57
96
|
# @param shown [Boolean] visible text had streamed (an `[interrupted]`
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -474,6 +474,20 @@ module Samagotchi
|
|
|
474
474
|
nil
|
|
475
475
|
end
|
|
476
476
|
|
|
477
|
+
# A session's LLM context strategy, apply rule and budget, each with
|
|
478
|
+
# where it came from (LLMContextStrategy::Explained#summary), as its
|
|
479
|
+
# worker would resolve them; nil when they can't be.
|
|
480
|
+
def llm_context_for(session)
|
|
481
|
+
registry = host_registry
|
|
482
|
+
name = session.model_name.to_s.strip.empty? ? ModelProfile.required_model_name(nil) : session.model_name
|
|
483
|
+
target = registry.resolve(name)
|
|
484
|
+
names = registry.lookup_names(session.model_typed || name, resolved: name, target: target)
|
|
485
|
+
own = session.llm_context
|
|
486
|
+
LLMContextStrategy.explain(target, names: names, **(own&.resolve_args || {})).summary(own)
|
|
487
|
+
rescue StandardError
|
|
488
|
+
nil
|
|
489
|
+
end
|
|
490
|
+
|
|
477
491
|
# Built again when config.yml's hosts: changed (compared as read: the
|
|
478
492
|
# YAML read is mtime-cached), so the model picker lists a host added
|
|
479
493
|
# while chi web runs. An injected registry (specs) stays.
|
|
@@ -612,7 +626,12 @@ module Samagotchi
|
|
|
612
626
|
# What the worker's server said it served for that model (after a turn).
|
|
613
627
|
if snapshot
|
|
614
628
|
session_json = session_json.merge(served_model: snapshot["served_model"], served_model_for: snapshot["served_model_for"])
|
|
629
|
+
# The model notes its prompt carries now (a /model there changes them).
|
|
630
|
+
session_json = session_json.merge(prompt_notes: snapshot["prompt_notes"]) if snapshot["prompt_notes"].is_a?(Array)
|
|
615
631
|
end
|
|
632
|
+
# The LLM context strategy the next turn runs under (the info bar's
|
|
633
|
+
# chip): the worker's, else worked out here from the session file.
|
|
634
|
+
session_json = session_json.merge(llm_context: (snapshot && snapshot["llm_context"]) || llm_context_for(session))
|
|
616
635
|
raw_messages = turn_snapshot ? turn_snapshot["messages"] : session.messages
|
|
617
636
|
timing = timing_payload(id, live_metrics: snapshot && snapshot["metrics"])
|
|
618
637
|
json_response(200, {
|
|
@@ -663,8 +682,9 @@ module Samagotchi
|
|
|
663
682
|
# history again (nor a render of every earlier answer). Live: the
|
|
664
683
|
# worker's GET tail alone (no session file read); else the file. The
|
|
665
684
|
# session part is what the page reads of it (id, status, used memory
|
|
666
|
-
# names
|
|
667
|
-
#
|
|
685
|
+
# names, the model notes its prompt carried: the first turn's build or
|
|
686
|
+
# a /model records them). ?turn_id=: that turn's answer (a queued
|
|
687
|
+
# turn's page re-reads after the next one started). ?recent=1 (the page since this
|
|
668
688
|
# release): the timing's records are the newest turn's (#recent_timing),
|
|
669
689
|
# which the page merges by id; without it, the whole timing as before.
|
|
670
690
|
def handle_tail_read(req, id)
|
|
@@ -675,12 +695,14 @@ module Samagotchi
|
|
|
675
695
|
tail = live_tail(id, turn_id: turn_id)
|
|
676
696
|
state = tail && tail["session_state_snapshot"]
|
|
677
697
|
if state.is_a?(Hash)
|
|
678
|
-
session_json = { id: id, status: state["status"], used_memory_names: Array(state["used_memory_names"])
|
|
698
|
+
session_json = { id: id, status: state["status"], used_memory_names: Array(state["used_memory_names"]),
|
|
699
|
+
prompt_notes: Array(state["prompt_notes"]) }
|
|
679
700
|
messages = last_assistant_for_display([tail["answer"]].compact)
|
|
680
701
|
else
|
|
681
702
|
session = @session_class.load(id, state_dir: default_state_dir)
|
|
682
703
|
state = nil
|
|
683
|
-
session_json = { id: id, status: displayed_status(session), used_memory_names: Array(session.used_memory_names)
|
|
704
|
+
session_json = { id: id, status: displayed_status(session), used_memory_names: Array(session.used_memory_names),
|
|
705
|
+
prompt_notes: session.prompt_notes.map(&:to_h) }
|
|
684
706
|
messages = last_assistant_for_display(session.messages, turn_id: turn_id)
|
|
685
707
|
end
|
|
686
708
|
live_metrics = state && state["metrics"]
|
|
@@ -822,7 +844,12 @@ module Samagotchi
|
|
|
822
844
|
when 202
|
|
823
845
|
record_history(body, line) if PromptHistory.shell_line?(line)
|
|
824
846
|
json_response(202, reply.json || { status: "accepted" })
|
|
825
|
-
when 400
|
|
847
|
+
when 400
|
|
848
|
+
# One of this chi's own commands the worker doesn't know: it runs an older chi.
|
|
849
|
+
name = reply.json&.dig("error") == "unknown_command" && SessionCommands.builtin_name(line)
|
|
850
|
+
next error_response(501, "not_supported", BridgeClient.stale_worker_message(id, cant: "run #{name}")) if name
|
|
851
|
+
|
|
852
|
+
error_response(400, reply.json&.dig("error") || "unknown_command", reply_detail(reply, "not a session command"))
|
|
826
853
|
end
|
|
827
854
|
end
|
|
828
855
|
rescue SessionManager::OwnedByTUI => e
|
|
@@ -1514,7 +1541,9 @@ module Samagotchi
|
|
|
1514
1541
|
# The context last seen and the token sums, for the ctx meter
|
|
1515
1542
|
# before the next turn streams (live over saved).
|
|
1516
1543
|
context: source["context"],
|
|
1517
|
-
tokens: source["tokens"]
|
|
1544
|
+
tokens: source["tokens"],
|
|
1545
|
+
# The memory indexes the session's prompt holds (the ctx tooltip).
|
|
1546
|
+
memory_index: source["memory_index"]
|
|
1518
1547
|
}
|
|
1519
1548
|
end
|
|
1520
1549
|
|
|
@@ -21,13 +21,14 @@ import {
|
|
|
21
21
|
addContext,
|
|
22
22
|
} from "./data.js";
|
|
23
23
|
import { createContextChips } from "./context_chips.js";
|
|
24
|
+
import { chipHtml as llmContextChipHtml, createLlmContextControl } from "./llm_context_chip.js";
|
|
24
25
|
import { renderStable } from "./stable_html.js";
|
|
25
26
|
import { createShownTexts } from "./shown_texts.js";
|
|
26
27
|
import { MODEL_KEY } from "./model_pick.js";
|
|
27
28
|
import { emptySessionState } from "./session_state.js";
|
|
28
29
|
import { createModelPicker } from "./model_picker.js";
|
|
29
30
|
import { chipName, chipsHtml, imageFiles, restoredChips, thumbsHtml, turnRefs, turnText } from "./images.js";
|
|
30
|
-
import { escapeHtml, memChipText, memChipTitle, noteHtml, normalize, steerRowHtml, ownerBadge, previewForCard, relativeTime, failedTurnText, modelLabel, deleteConfirmText, recapLabel, canStopSession, goneSessionNotice, withLiveStatus, withoutWorker, recapPlace, delegatedBy, childrenSummary } from "./format.js";
|
|
31
|
+
import { escapeHtml, memChipText, memChipTitle, noteHtml, normalize, steerRowHtml, ownerBadge, previewForCard, relativeTime, failedTurnText, modelLabel, deleteConfirmText, recapLabel, canStopSession, goneSessionNotice, withLiveStatus, withoutWorker, recapPlace, delegatedBy, childrenSummary, promptNotesChip } from "./format.js";
|
|
31
32
|
import { createTurnView, turnHistoryHtml } from "./turn_view.js";
|
|
32
33
|
import { BADGES, emptyAnswerHtml, htmlElement, stateBadgeHtml, userBubbleHtml } from "./turn_html.js";
|
|
33
34
|
import { createStageView } from "./stage_view.js";
|
|
@@ -75,6 +76,16 @@ const contextChips = createContextChips({
|
|
|
75
76
|
escapeHtml,
|
|
76
77
|
toast: (text) => showToast(text),
|
|
77
78
|
});
|
|
79
|
+
// The open session's LLM context chip in the info bar, and its popover:
|
|
80
|
+
// /llm-context runs in the session's worker.
|
|
81
|
+
const llmContextControl = createLlmContextControl({
|
|
82
|
+
el: infoTextEl,
|
|
83
|
+
escapeHtml,
|
|
84
|
+
summary: () => (selected ? current.llmContext : null),
|
|
85
|
+
session: () => selected,
|
|
86
|
+
running: () => liveTurn.running,
|
|
87
|
+
run: (line) => runSessionCommand(line),
|
|
88
|
+
});
|
|
78
89
|
const markdownWarningEl = $("#markdownWarning");
|
|
79
90
|
// What the page renders that a notice may stand in for (the server's
|
|
80
91
|
// Web::Capabilities, next to markdown_warning); a notice it names isn't
|
|
@@ -353,7 +364,7 @@ function cardHtml(s, { deletable = false, archivable = false, selectable = false
|
|
|
353
364
|
const folderHtml = folder ? `<span class="folder" title="${escapeHtml(s.working_directory)}">${escapeHtml(folder)}</span>` : "";
|
|
354
365
|
const archivedHtml = s.archived ? `<span class="archived-badge" title="Archived: hidden from the lists, kept for good">archived</span>` : "";
|
|
355
366
|
const ctx = cardCtxText(s.ctx_pct);
|
|
356
|
-
const ctxHtml = ctx ? `<span class="ctx" title="${escapeHtml(tokensTipText(s.tokens))}">${escapeHtml(ctx)}</span>` : "";
|
|
367
|
+
const ctxHtml = ctx ? `<span class="ctx" title="${escapeHtml(tokensTipText(s.tokens, undefined, s.memory_index))}">${escapeHtml(ctx)}</span>` : "";
|
|
357
368
|
const waiting = waitingBadge(s);
|
|
358
369
|
const waitingHtml = waiting ? `<span class="attn attn-${waiting.kind}" title="${escapeHtml(waiting.title)}">${escapeHtml(waiting.text)}</span>` : "";
|
|
359
370
|
// A turn a hook stopped wears its own badge, unless the session waits on
|
|
@@ -1115,6 +1126,9 @@ function renderSessionView(data) {
|
|
|
1115
1126
|
current.usedMemories = s.used_memory_names || [];
|
|
1116
1127
|
current.preloadedMemories = s.preloaded_memory_names || [];
|
|
1117
1128
|
current.mutedMemories = s.muted_memory_names || [];
|
|
1129
|
+
current.promptNotes = Array.isArray(s.prompt_notes) ? s.prompt_notes : [];
|
|
1130
|
+
current.llmContext = s.llm_context || null;
|
|
1131
|
+
llmContextControl.refresh();
|
|
1118
1132
|
current.parentId = s.parent_id || null;
|
|
1119
1133
|
current.commands = Array.isArray(data.commands) ? data.commands : [];
|
|
1120
1134
|
if (data.messages && data.messages.length) {
|
|
@@ -1219,6 +1233,8 @@ async function readTurnEnd() {
|
|
|
1219
1233
|
if (id !== selected) return;
|
|
1220
1234
|
if (d.session) {
|
|
1221
1235
|
if (Array.isArray(d.session.used_memory_names)) current.usedMemories = d.session.used_memory_names;
|
|
1236
|
+
// The notes chip: the first turn's build (or a /model) records them.
|
|
1237
|
+
if (Array.isArray(d.session.prompt_notes)) current.promptNotes = d.session.prompt_notes;
|
|
1222
1238
|
if (d.session.status) current.status = d.session.status;
|
|
1223
1239
|
if (!(await mergeTurnTiming(id, d.timing))) return;
|
|
1224
1240
|
liveTurn.timing.finishLine(line);
|
|
@@ -1752,7 +1768,7 @@ function updateInfoBar() {
|
|
|
1752
1768
|
const memText = current.usedMemories.length ? current.usedMemories.join(", ") : "—";
|
|
1753
1769
|
const ctxText = current.ctxPct !== null ? `ctx ${Math.round(current.ctxPct)}%` : "";
|
|
1754
1770
|
const tokens = current.timing.tokens;
|
|
1755
|
-
const ctxTip = tokensTipText(tokens);
|
|
1771
|
+
const ctxTip = tokensTipText(tokens, undefined, current.timing.memoryIndex);
|
|
1756
1772
|
const speedTextNow = lastSpeedText(tokens);
|
|
1757
1773
|
const avgSpeed = speedText(tokens?.avg_decode_tps, tokens?.tps_source);
|
|
1758
1774
|
const speedTip = `Decode speed of the last generation${avgSpeed ? `; session average ${avgSpeed}` : ""}`;
|
|
@@ -1765,11 +1781,14 @@ function updateInfoBar() {
|
|
|
1765
1781
|
const parentHtml = parentChip ? `<a class="model parent" href="${escapeHtml(sessionHash(current.parentId))}" title="${escapeHtml(parentChip.title)}">delegated by ${escapeHtml(String(current.parentId).slice(0, 8))}</a>` : "";
|
|
1766
1782
|
// A parent names its delegates; the link opens all sessions.
|
|
1767
1783
|
const kids = childrenSummary({ id: selected }, allSessions);
|
|
1784
|
+
// The model notes the session's prompt carried.
|
|
1785
|
+
const notes = promptNotesChip(current.promptNotes);
|
|
1786
|
+
const notesHtml = notes ? `<span class="model notes-chip" title="${escapeHtml(notes.title)}">${escapeHtml(notes.text)}</span>` : "";
|
|
1768
1787
|
const kidsHtml = kids ? `<a class="model children-chip${kids.waitingText ? " waiting" : ""}" href="${escapeHtml(ALL_SESSIONS_HASH)}" title="${escapeHtml(`${kids.title} (all sessions)`)}">${escapeHtml(kids.text)}${kids.waitingText ? ` · <span class="kids-waiting">${escapeHtml(kids.waitingText)}</span>` : ""}</a>` : "";
|
|
1769
1788
|
const attachCmd = `chi --attach ${selected}`;
|
|
1770
1789
|
const copied = Date.now() - attachCopiedAt < 2000;
|
|
1771
1790
|
const copyHtml = `<button type="button" class="copy-attach" title="Copy "${escapeHtml(attachCmd)}" to attach a terminal">${copied ? "copied ✓" : "copy chi --attach"}</button>`;
|
|
1772
|
-
const metaHtml = `<div class="meta"><span class="id" title="${escapeHtml(selected)}">${escapeHtml(idShort)}</span>${copyHtml}<span class="${statusClass}">${escapeHtml(current.status||"")}</span><span class="model${label.mismatch ? " served-mismatch" : ""}" title="${escapeHtml(label.title)}">${escapeHtml(label.text)}</span>${parentHtml}${kidsHtml}${current.usedMemories.length ? `<span class="model mem-chip" title="${escapeHtml(memChipTitle(current.usedMemories))}">${escapeHtml(memChipText(current.usedMemories))}</span>` : ""}<span class="dir" title="${escapeHtml(current.dir||"")}"><bdi>${escapeHtml(current.dir||"")}</bdi></span>${ctxText ? `<span class="model ctx" title="${escapeHtml(ctxTip)}">${escapeHtml(ctxText)}</span>` : ""}${speedTextNow ? `<span class="model speed" title="${escapeHtml(speedTip)}">${escapeHtml(speedTextNow)}</span>` : ""}${timingText ? `<span class="model session-time"></span>` : ""}${workerBadgeHtml()}</div>`;
|
|
1791
|
+
const metaHtml = `<div class="meta"><span class="id" title="${escapeHtml(selected)}">${escapeHtml(idShort)}</span>${copyHtml}<span class="${statusClass}">${escapeHtml(current.status||"")}</span><span class="model${label.mismatch ? " served-mismatch" : ""}" title="${escapeHtml(label.title)}">${escapeHtml(label.text)}</span>${parentHtml}${kidsHtml}${llmContextChipHtml(current.llmContext, escapeHtml)}${notesHtml}${current.usedMemories.length ? `<span class="model mem-chip" title="${escapeHtml(memChipTitle(current.usedMemories))}">${escapeHtml(memChipText(current.usedMemories))}</span>` : ""}<span class="dir" title="${escapeHtml(current.dir||"")}"><bdi>${escapeHtml(current.dir||"")}</bdi></span>${ctxText ? `<span class="model ctx" title="${escapeHtml(ctxTip)}">${escapeHtml(ctxText)}</span>` : ""}${speedTextNow ? `<span class="model speed" title="${escapeHtml(speedTip)}">${escapeHtml(speedTextNow)}</span>` : ""}${timingText ? `<span class="model session-time"></span>` : ""}${workerBadgeHtml()}</div>`;
|
|
1773
1792
|
const previewHtml = `<div class="preview-line"><span class="preview">${escapeHtml(current.firstPreview || "—")}</span> · <span class="mem" title="${escapeHtml(current.usedMemories.join(", "))}">mem: ${escapeHtml(memText)}</span></div>`;
|
|
1774
1793
|
// The session clock ticks every second while a turn runs: only its text
|
|
1775
1794
|
// changes, the bar's other nodes (a tooltip, a button mid-press) stay.
|
|
@@ -2006,10 +2025,13 @@ const streamHandlers = {
|
|
|
2006
2025
|
},
|
|
2007
2026
|
// The turn raised (e.g. the model server stayed unreachable). The worker
|
|
2008
2027
|
// stays up: it rolls the turn back and hands the prompt back
|
|
2009
|
-
// (prompt_restored), and the prompts queued behind it run as usual.
|
|
2028
|
+
// (prompt_restored), and the prompts queued behind it run as usual. A turn
|
|
2029
|
+
// that got to tool steps stays as it is (kept_steps; no prompt_restored).
|
|
2010
2030
|
turn_failed: (data) => {
|
|
2011
|
-
|
|
2012
|
-
|
|
2031
|
+
const text = failedTurnText(data);
|
|
2032
|
+
// Only a rolled-back turn's line waits for its prompt_restored.
|
|
2033
|
+
liveTurn.failedText = Number.isInteger(data.kept_steps) ? null : text;
|
|
2034
|
+
endTurnView("failed", text);
|
|
2013
2035
|
setLiveStatus("idle");
|
|
2014
2036
|
},
|
|
2015
2037
|
// The rolled-back turn left the session: render it from the server again,
|
|
@@ -2047,6 +2069,12 @@ const streamHandlers = {
|
|
|
2047
2069
|
current.served = null;
|
|
2048
2070
|
updateInfoBar();
|
|
2049
2071
|
}
|
|
2072
|
+
// /llm-context changed what the next turn runs under.
|
|
2073
|
+
if (data.llm_context) {
|
|
2074
|
+
current.llmContext = data.llm_context;
|
|
2075
|
+
updateInfoBar();
|
|
2076
|
+
llmContextControl.refresh();
|
|
2077
|
+
}
|
|
2050
2078
|
if (view.resync) await resync();
|
|
2051
2079
|
// A card's action that isn't anytime (the step-limit question's
|
|
2052
2080
|
// /continue) has no bubble: nothing to remove.
|
|
@@ -2320,6 +2348,18 @@ async function handleSendCommand(line) {
|
|
|
2320
2348
|
}
|
|
2321
2349
|
}
|
|
2322
2350
|
|
|
2351
|
+
// A session command from a control (the llm ctx chip), not the composer:
|
|
2352
|
+
// its command_ran shows its line and reply, as a typed one's would.
|
|
2353
|
+
async function runSessionCommand(line) {
|
|
2354
|
+
if (!selected) return;
|
|
2355
|
+
try {
|
|
2356
|
+
await sendCommand(selected, line, { clientId });
|
|
2357
|
+
followWokenWorker();
|
|
2358
|
+
} catch (e) {
|
|
2359
|
+
addCommandBubble({ label: null, line, text: e.message, failed: true });
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2323
2363
|
function addCommandBubble(view) {
|
|
2324
2364
|
removeHintIfEmpty();
|
|
2325
2365
|
const div = document.createElement("div");
|
|
@@ -71,14 +71,37 @@ function count(n) {
|
|
|
71
71
|
return Math.round(n).toLocaleString("en-US");
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// A token count as the size readouts word it: "450", "2.0k". As
|
|
75
|
+
// MemoryBundle::IndexSize.count_text.
|
|
76
|
+
function countK(n) {
|
|
77
|
+
return n >= 1000 ? `${(Math.round(n / 100) / 10).toFixed(1)}k` : String(Math.round(n));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// "~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)": the
|
|
81
|
+
// memory indexes the session's prompt holds (the snapshot's memory_index,
|
|
82
|
+
// measured when the prompt was built); "" without one. As
|
|
83
|
+
// Formatting#memory_index_text (spec/shared/labels_matrix.json).
|
|
84
|
+
export function memoryIndexText(memoryIndex) {
|
|
85
|
+
if (!memoryIndex || typeof memoryIndex !== "object") return "";
|
|
86
|
+
const scopes = ["system", "project"]
|
|
87
|
+
.map((scope) => [scope, num(memoryIndex[scope]?.tokens)])
|
|
88
|
+
.filter(([, tokens]) => tokens !== null);
|
|
89
|
+
if (!scopes.length) return "";
|
|
90
|
+
const total = scopes.reduce((sum, [, tokens]) => sum + tokens, 0);
|
|
91
|
+
return `~${countK(total)} tokens in this session's prompt (${scopes.map(([scope, tokens]) => `${scope} ${countK(tokens)}`).join(", ")})`;
|
|
92
|
+
}
|
|
93
|
+
|
|
74
94
|
// The ctx tooltip (info bar and card): +title+, then the session's tokens
|
|
75
95
|
// (in summed over every request, cached with its share, out with the
|
|
76
|
-
// reasoning part) and its cost, when it has any
|
|
77
|
-
// its
|
|
78
|
-
|
|
96
|
+
// reasoning part) and its cost, when it has any, and the memory indexes
|
|
97
|
+
// its prompt holds (+memoryIndex+, the snapshot's memory_index). A
|
|
98
|
+
// delegated session keeps its own metrics, so these are this session's only.
|
|
99
|
+
export function tokensTipText(tokens, title = "Context used after the last turn", memoryIndex = null) {
|
|
100
|
+
const memory = memoryIndexText(memoryIndex);
|
|
101
|
+
const memoryLine = memory ? `\nmemory index: ${memory}` : "";
|
|
79
102
|
const prompt = num(tokens?.prompt_sum) || 0;
|
|
80
103
|
const completion = num(tokens?.completion_sum) || 0;
|
|
81
|
-
if (!prompt && !completion) return title
|
|
104
|
+
if (!prompt && !completion) return `${title}${memoryLine}`;
|
|
82
105
|
const cached = num(tokens.cached_sum) || 0;
|
|
83
106
|
const reasoning = num(tokens.reasoning_sum) || 0;
|
|
84
107
|
const parts = [`in ${count(prompt)}`];
|
|
@@ -88,7 +111,7 @@ export function tokensTipText(tokens, title = "Context used after the last turn"
|
|
|
88
111
|
const cost = costText(tokens.cost_sum);
|
|
89
112
|
if (cost) lines.push(`cost: ${cost}`);
|
|
90
113
|
lines.push("this session only, all requests");
|
|
91
|
-
return lines.join("\n")
|
|
114
|
+
return `${lines.join("\n")}${memoryLine}`;
|
|
92
115
|
}
|
|
93
116
|
|
|
94
117
|
// The info bar's speed: the newest generation's decode speed, "" before one.
|
|
@@ -234,11 +234,14 @@ export function recapPlace(userBubbles, turnsSince) {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
// The line a failed turn's bubble shows: a provider error's one-line
|
|
237
|
-
// summary, else the error's message and class.
|
|
237
|
+
// summary, else the error's message and class. A turn whose work stayed
|
|
238
|
+
// (turn_failed's kept_steps; no prompt_restored follows) says so, as the
|
|
239
|
+
// terminal's rollback hint does.
|
|
238
240
|
export function failedTurnText(data) {
|
|
239
241
|
data = data || {};
|
|
240
242
|
const detail = data.summary || `${data.message || "error"}${data.error_class ? ` (${data.error_class})` : ""}`;
|
|
241
|
-
|
|
243
|
+
const kept = Number.isInteger(data.kept_steps) ? "; partial progress kept (!rollback restores the pre-turn state)" : "";
|
|
244
|
+
return `\u2715 turn failed: ${detail}${kept}`;
|
|
242
245
|
}
|
|
243
246
|
|
|
244
247
|
// Whether the server served another model than asked: names equal in any
|
|
@@ -298,3 +301,27 @@ export function memChipTitle(names) {
|
|
|
298
301
|
const list = names || [];
|
|
299
302
|
return list.length ? `memories: ${list.join(", ")}` : "no memories in this session";
|
|
300
303
|
}
|
|
304
|
+
|
|
305
|
+
// "model_notes_deepseek (system, 612 chars), model_notes_x (project, 80
|
|
306
|
+
// chars)": the model notes the session's prompt carried (its prompt_notes);
|
|
307
|
+
// "" without any. As PromptNote.text (spec/shared/labels_matrix.json).
|
|
308
|
+
export function promptNotesText(notes) {
|
|
309
|
+
return (Array.isArray(notes) ? notes : [])
|
|
310
|
+
.filter((note) => note && typeof note.name === "string" && note.name.trim())
|
|
311
|
+
.map((note) => {
|
|
312
|
+
const details = [note.scope, Number.isFinite(note.chars) ? `${Math.trunc(note.chars)} chars` : null].filter(Boolean);
|
|
313
|
+
return details.length ? `${note.name.trim()} (${details.join(", ")})` : note.name.trim();
|
|
314
|
+
})
|
|
315
|
+
.join(", ");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// The info bar's model notes chip, for a session whose prompt carried
|
|
319
|
+
// some: "notes: deepseek" (the names without model_notes_), the full line
|
|
320
|
+
// its tooltip; null without notes.
|
|
321
|
+
export function promptNotesChip(notes) {
|
|
322
|
+
const text = promptNotesText(notes);
|
|
323
|
+
if (!text) return null;
|
|
324
|
+
const names = notes.filter((note) => note && typeof note.name === "string" && note.name.trim())
|
|
325
|
+
.map((note) => note.name.trim().replace(/^model_notes_/, ""));
|
|
326
|
+
return { text: `notes: ${names.join(", ")}`, title: `model notes in this session's prompt: ${text}` };
|
|
327
|
+
}
|
|
@@ -684,6 +684,18 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
684
684
|
#infoBar #infoArchiveBtn:hover{color:var(--text);background:var(--bg-hover)}
|
|
685
685
|
#infoBar #infoDeleteBtn{background:transparent;color:var(--text-muted);border:1px solid transparent}
|
|
686
686
|
#infoBar #infoDeleteBtn:hover{color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
|
|
687
|
+
/* The LLM context chip (llm_context_chip.js): a model-like chip that opens the strategy popover; accent when the session set its own. */
|
|
688
|
+
#infoBar .meta .llmctx-chip{padding:2px 6px;border-radius:6px;font-size:11px;font-weight:400;border:1px solid transparent;cursor:pointer;font-family:inherit;white-space:nowrap}
|
|
689
|
+
#infoBar .meta .llmctx-chip:hover{color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
690
|
+
#infoBar .meta .llmctx-chip.own{color:var(--accent);border-color:var(--accent-border)}
|
|
691
|
+
.llmctx-form{display:flex;flex-direction:column;gap:8px;margin:0}
|
|
692
|
+
.llmctx-form .ctx-pop-head strong{font-family:inherit}
|
|
693
|
+
.llmctx-field{display:flex;align-items:center;gap:8px}
|
|
694
|
+
.llmctx-field>span{flex:0 0 58px;font-size:12px;color:var(--text-muted)}
|
|
695
|
+
.llmctx-field select,.llmctx-field input{flex:1;min-width:0;box-sizing:border-box;padding:5px 8px;border-radius:7px;border:1px solid var(--border-strong);background:var(--bg-inset);color:var(--text);font:13px/1.4 inherit}
|
|
696
|
+
.llmctx-field select:focus,.llmctx-field input:focus{outline:none;border-color:var(--accent-border)}
|
|
697
|
+
.llmctx-form .llmctx-set{background:var(--accent-bg);color:var(--accent);border-color:var(--accent-border)}
|
|
698
|
+
.llmctx-form .llmctx-set:disabled{opacity:.6;cursor:default}
|
|
687
699
|
/* Attached context (context_chips.js): a row of its own under the meta; not the composer's #chips/.chip. */
|
|
688
700
|
#contextChips{order:5;flex-basis:100%;display:flex;flex-wrap:wrap;gap:6px;align-items:center;min-width:0}
|
|
689
701
|
#contextChips[hidden]{display:none}
|