samagotchi 0.154.0 → 0.155.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 +30 -1
- data/docs/architecture.md +3 -2
- data/docs/bundles.md +21 -0
- data/docs/cli.md +10 -8
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +4 -4
- data/docs/internals/attached-context.md +8 -6
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/bridge.md +18 -5
- data/docs/internals/engine.md +221 -100
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +12 -6
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +3 -3
- data/docs/testing.md +16 -1
- data/lib/samagotchi/answer_text.rb +1 -2
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bridge/card_places.rb +56 -7
- data/lib/samagotchi/bridge/card_store.rb +24 -9
- data/lib/samagotchi/bridge.rb +9 -0
- data/lib/samagotchi/bridge_client.rb +9 -5
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +1 -1
- data/lib/samagotchi/engine/context_judge.rb +19 -125
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/objectives.rb +22 -90
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +5 -2
- data/lib/samagotchi/engine/toolkit.rb +33 -190
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +162 -99
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/engine_snapshots.rb +9 -8
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +6 -4
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/idle_recap.rb +3 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +194 -216
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +66 -42
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/session_manager.rb +8 -13
- data/lib/samagotchi/session_retention.rb +24 -4
- data/lib/samagotchi/steer.rb +12 -1
- data/lib/samagotchi/stream_event.rb +22 -0
- data/lib/samagotchi/system_prompt.rb +84 -61
- data/lib/samagotchi/terminal_ui/attached_view.rb +0 -9
- data/lib/samagotchi/terminal_ui/input_support.rb +16 -15
- data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
- data/lib/samagotchi/terminal_ui/reline_seam.rb +0 -10
- data/lib/samagotchi/terminal_ui/status_row.rb +0 -7
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/tool_activity.rb +8 -6
- data/lib/samagotchi/tool_declarations.rb +4 -4
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +93 -101
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/registry.rb +43 -40
- data/lib/samagotchi/tools/tool_index.rb +135 -88
- data/lib/samagotchi/tools/tool_surface.rb +173 -0
- data/lib/samagotchi/turn_context.rb +67 -0
- data/lib/samagotchi/turn_flow.rb +7 -2
- data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
- data/lib/samagotchi/turn_stages/objectives.rb +146 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
- data/lib/samagotchi/turn_stages/warmup.rb +83 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -1
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +8 -1
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- data/lib/samagotchi/web/public/card_stamps.js +27 -0
- data/lib/samagotchi/web/public/cards_place.js +5 -3
- data/lib/samagotchi/web/public/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/composer_mode.js +1 -1
- data/lib/samagotchi/web/public/format.js +3 -2
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +8 -4
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- data/lib/samagotchi/web/session_access_routes.rb +8 -0
- data/lib/samagotchi/web/session_show.rb +4 -0
- data/lib/samagotchi/worker.rb +8 -5
- data/lib/samagotchi.rb +0 -1
- metadata +19 -2
- data/lib/samagotchi/pending_input_queue.rb +0 -55
|
@@ -26,7 +26,9 @@ module Samagotchi
|
|
|
26
26
|
# holds the id of the turn it belongs to or follows (+turn_id+: the running
|
|
27
27
|
# turn's, else the last one's, nil before any) and lists with +turns_since+,
|
|
28
28
|
# the turns drawn after it, counted from the messages the page draws
|
|
29
|
-
# (CardPlaces, with the turn log the store keeps),
|
|
29
|
+
# (CardPlaces, with the turn log the store keeps), +opening_id+, the same
|
|
30
|
+
# place as a stamp (the id of the opening it comes after, which the page
|
|
31
|
+
# finds by its bubble; none where only the count can say), and +current+ for a card
|
|
30
32
|
# of the turn running now. A card that isn't the turn's but comes while one
|
|
31
33
|
# runs (an anytime command's, such as /btw) goes after that turn: its prompt
|
|
32
34
|
# is already in the history. Listed against messages, an in-turn entry of a
|
|
@@ -70,7 +72,7 @@ module Samagotchi
|
|
|
70
72
|
SAVED_ON = (%i[turn_started card hook_notice empty_answer_retry steer_cut llm_context_edited question_requested question_answered
|
|
71
73
|
question_cancelled question_relay] + Events::TURN_END).freeze
|
|
72
74
|
# What only the store reads: never listed.
|
|
73
|
-
INTERNAL_FIELDS = %i[turn_id continued base_since past_idless during after_failed command].freeze
|
|
75
|
+
INTERNAL_FIELDS = %i[turn_id continued base_since past_idless after_opening during after_failed command].freeze
|
|
74
76
|
|
|
75
77
|
# What a session's saved file lists, as a store started from it lists
|
|
76
78
|
# (no turn running): the web's cards for a session no worker runs.
|
|
@@ -118,7 +120,8 @@ module Samagotchi
|
|
|
118
120
|
# them (a body re-read, a trace): no turns_since:, nothing to tell a turn
|
|
119
121
|
# that left nothing by.
|
|
120
122
|
# @return [Array<Hash>] oldest first: the card's (or notice's) fields
|
|
121
|
-
# plus current:, turns_since:
|
|
123
|
+
# plus current:, turns_since: and opening_id: (CardPlaces#opening_of,
|
|
124
|
+
# when it has one) with +messages+, earlier: true for one an
|
|
122
125
|
# earlier worker saved, during: true for a card that came while the
|
|
123
126
|
# running turn runs but isn't that turn's (it goes after the running
|
|
124
127
|
# turn's prompt), and after_failed: true for one shown after the last
|
|
@@ -141,6 +144,8 @@ module Samagotchi
|
|
|
141
144
|
|
|
142
145
|
out = shown.except(*INTERNAL_FIELDS)
|
|
143
146
|
out[:turns_since] = places.turns_since(entry) if places
|
|
147
|
+
stamp = places&.opening_of(entry)
|
|
148
|
+
out[:opening_id] = stamp if stamp
|
|
144
149
|
out[:current] = current
|
|
145
150
|
out[:during] = true if entry[:during]
|
|
146
151
|
out[:after_failed] = true if entry[:after_failed] && entry[:turn_id] == @failed_turn
|
|
@@ -255,7 +260,7 @@ module Samagotchi
|
|
|
255
260
|
# A hand-edited or foreign file's entry goes, rather than break every later
|
|
256
261
|
# listing of the session.
|
|
257
262
|
def sound_place?(entry)
|
|
258
|
-
return false unless entry[:turn_id].nil? ||
|
|
263
|
+
return false unless [entry[:turn_id], entry[:after_opening]].all? { |id| id.nil? || id.is_a?(String) }
|
|
259
264
|
|
|
260
265
|
entry.values_at(:base_since, :past_idless).compact.all? { |count| count.is_a?(Integer) && !count.negative? }
|
|
261
266
|
end
|
|
@@ -315,8 +320,8 @@ module Samagotchi
|
|
|
315
320
|
shown = @entries.select { |entry| entry[:turn_id] == @running_turn && !entry[:in_turn] }
|
|
316
321
|
return if shown.empty? || !@messages
|
|
317
322
|
|
|
318
|
-
|
|
319
|
-
shown.each { |entry| entry
|
|
323
|
+
places = CardPlaces.new(@turn_log, @messages.call)
|
|
324
|
+
shown.each { |entry| note_idless(entry, places) }
|
|
320
325
|
end
|
|
321
326
|
|
|
322
327
|
# The entry's anchor: the running turn, else the last one.
|
|
@@ -331,7 +336,7 @@ module Samagotchi
|
|
|
331
336
|
# button's command replaced, which nobody typed).
|
|
332
337
|
old = @entries[index]
|
|
333
338
|
@entries[index] = card.except(:command).merge(updated: true, **old.slice(:in_turn, :turn_id, :during, :continued, :base_since,
|
|
334
|
-
:past_idless, :after_failed, :command))
|
|
339
|
+
:past_idless, :after_opening, :after_failed, :command))
|
|
335
340
|
else
|
|
336
341
|
entry = card.merge(in_turn: event[:in_turn] ? true : false, turn_id: anchor)
|
|
337
342
|
entry[:during] = true if @running_turn && !event[:in_turn]
|
|
@@ -393,8 +398,18 @@ module Samagotchi
|
|
|
393
398
|
def note_past_idless(entry)
|
|
394
399
|
return unless @messages
|
|
395
400
|
|
|
396
|
-
|
|
397
|
-
|
|
401
|
+
note_idless(entry, CardPlaces.new(@turn_log, @messages.call))
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# How many openings with no turn id +entry+ came after, and the last
|
|
405
|
+
# one's own id when it has one (+after_opening+: its stamp, #list).
|
|
406
|
+
def note_idless(entry, places)
|
|
407
|
+
run = places.idless_after(entry[:turn_id])
|
|
408
|
+
return unless run.positive?
|
|
409
|
+
|
|
410
|
+
entry[:past_idless] = run
|
|
411
|
+
after = places.after_opening(entry[:turn_id])
|
|
412
|
+
entry[:after_opening] = after if after
|
|
398
413
|
end
|
|
399
414
|
|
|
400
415
|
# Which cap +entry+ counts against: :notice, :llm_context or :card.
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -196,6 +196,15 @@ module Samagotchi
|
|
|
196
196
|
@mutex.synchronize { @open_streams - (client_id ? @streams_by_client[client_id] : 0) }
|
|
197
197
|
end
|
|
198
198
|
|
|
199
|
+
# Streams held by the clients whose id starts with +prefix+
|
|
200
|
+
# (ClientId::TUI_PREFIX: the attached terminals, which a scratch worker
|
|
201
|
+
# counts, Worker#scratch_unattended?). A stream that named no client
|
|
202
|
+
# (a web tab's, through the chi web proxy) is none of them.
|
|
203
|
+
# @return [Integer]
|
|
204
|
+
def open_streams_of(prefix)
|
|
205
|
+
@mutex.synchronize { @streams_by_client.sum { |client_id, count| client_id.start_with?(prefix) ? count : 0 } }
|
|
206
|
+
end
|
|
207
|
+
|
|
199
208
|
# Monotonic time of the last request, stream open or stream close. The
|
|
200
209
|
# worker's idle exit counts from it. A connect that sends no request (the
|
|
201
210
|
# sidecar liveness probe) doesn't count.
|
|
@@ -309,12 +309,16 @@ module Samagotchi
|
|
|
309
309
|
# older worker's 404 for a route it lacks from a refused connect, a
|
|
310
310
|
# timeout or a 500.
|
|
311
311
|
# @return [Array(Integer, Object)] the status and the parsed JSON body
|
|
312
|
-
# (nil when it isn't JSON); [nil, nil] when no reply came
|
|
312
|
+
# (nil when it isn't JSON); [nil, nil] when no reply came (a refused
|
|
313
|
+
# connect, a timeout, a cut or garbled reply), and when anything else
|
|
314
|
+
# failed, which is logged
|
|
313
315
|
def get(path)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
Degrade.call(:bridge, "get_failed", fallback: [nil, nil], path: path) do
|
|
317
|
+
response = get_response(path)
|
|
318
|
+
[response.status, response.json]
|
|
319
|
+
rescue SystemCallError, IOError
|
|
320
|
+
[nil, nil]
|
|
321
|
+
end
|
|
318
322
|
end
|
|
319
323
|
|
|
320
324
|
# GET /session/:id/<path>, preserving the raw response and raising on I/O.
|
|
@@ -421,7 +421,7 @@ module Samagotchi
|
|
|
421
421
|
end
|
|
422
422
|
end
|
|
423
423
|
{ "init" => registered.inits, "toolkit" => registered.toolkits, "tool source" => registered.tool_sources,
|
|
424
|
-
"peer source" => registered.peer_sources }.each do |what, names|
|
|
424
|
+
"peer source" => registered.peer_sources, "prompt section" => registered.prompt_sections }.each do |what, names|
|
|
425
425
|
names.each { |name| add(:info, :registered, plugin, "#{what} #{name}") }
|
|
426
426
|
end
|
|
427
427
|
end
|
|
@@ -124,7 +124,7 @@ module Samagotchi
|
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
# The listing's placement fields: nothing to place in a one-turn run.
|
|
127
|
-
LISTING_FIELDS = %w[turns_since current earlier after_failed label].freeze
|
|
127
|
+
LISTING_FIELDS = %w[turns_since opening_id current earlier after_failed label].freeze
|
|
128
128
|
|
|
129
129
|
# The saved cards as the web reads them (CardStore.saved), with
|
|
130
130
|
# string keys and values, as the JSON trace prints them.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: btw
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.5
|
|
4
4
|
scope: system
|
|
5
5
|
description: "/btw <question>: a side answer about the conversation, as a card in every UI, even mid-turn; Keep as session forks it into a child session"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
plugin:
|
|
8
8
|
file: plugin.rb
|
|
9
|
-
sha256: sha256:
|
|
9
|
+
sha256: sha256:d4c0d6309308270a1430d64f9713e46c15d71ace1b068fefc22916aa8cc2d043
|
|
10
10
|
requires_chi: ">= 0.143.0"
|
|
11
11
|
# config.yml bundles: btw: (another key warns; an older chi ignores this)
|
|
12
12
|
settings: [max_tokens, timeout]
|
|
@@ -15,7 +15,7 @@ class Plugin
|
|
|
15
15
|
SYSTEM = "Answer the user's question about the conversation below, briefly. The user asks it on the side: " \
|
|
16
16
|
"don't continue the conversation's task, and don't say what you would do next."
|
|
17
17
|
# How many answers /btw keep can still fork: the last ones, for the
|
|
18
|
-
# worker's
|
|
18
|
+
# worker's life.
|
|
19
19
|
KEPT = 10
|
|
20
20
|
TITLE_CHARS = 72
|
|
21
21
|
EARLIER = "(About the conversation before the running turn.)"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: coordinator
|
|
3
|
-
version: 0.8.
|
|
3
|
+
version: 0.8.1
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, merges under a guardrail approval (the git-merge rule) or a question, a handoff memory to resume from), /coordinate <goal>, /coordinate resume, /coordinate end, /coordinate review (experimental: a sprint review session to share) and /children; the coordinator toolkit (lean + delegate, delegate_result, list_sessions, objectives) and the coding toolkit for build children (child + be concise)"
|
|
6
6
|
trust_level: reviewed
|
|
@@ -10,7 +10,7 @@ files:
|
|
|
10
10
|
description: "Coordinate parallel work: split it, delegate each task wait:false in its own worktree (cwd:), check each report, merge under the git-merge approval or ask the user; resume from a handoff_* memory"
|
|
11
11
|
plugin:
|
|
12
12
|
file: plugin.rb
|
|
13
|
-
sha256: sha256:
|
|
13
|
+
sha256: sha256:540dd6369342ea4dcbc6ba5ee17fb20d980b880df614a475668c70115e7ec6b8
|
|
14
14
|
# the coordinator toolkit needs chi.toolkit and ctx.use_toolkit (0.66.0); the
|
|
15
15
|
# coding toolkit (chi.toolkit prompt:) and the skill's delegate toolkit: need
|
|
16
16
|
# the next chi.
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
# (ctx.sessions.stop, by session id) and shows the card again.
|
|
17
17
|
#
|
|
18
18
|
# /coordinate <goal> asks the model, in this session, to follow the skill
|
|
19
|
-
# for the goal. A
|
|
20
|
-
# is shown for the user
|
|
19
|
+
# for the goal. A session a one-turn run holds (-p --non-interactive, or an
|
|
20
|
+
# older chi's terminal) takes no messages: the request is shown for the user
|
|
21
|
+
# to send.
|
|
21
22
|
#
|
|
22
23
|
# /coordinate resume picks up a coordinator handoff: the open handoff_*
|
|
23
24
|
# memories of this project (an index line whose description doesn't start
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dev
|
|
3
|
-
version: 0.7.
|
|
3
|
+
version: 0.7.8
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Developer tools"
|
|
6
6
|
trust_level: reviewed
|
|
7
|
-
requires_chi: ">= 0.
|
|
7
|
+
requires_chi: ">= 0.155.0"
|
|
8
8
|
includes:
|
|
9
9
|
- known-names
|
|
10
10
|
- mcp
|
|
@@ -18,3 +18,4 @@ includes:
|
|
|
18
18
|
- ruby
|
|
19
19
|
- model-notes
|
|
20
20
|
- decision-card
|
|
21
|
+
- inline-widgets
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: github-pr
|
|
3
|
-
version: 0.6.
|
|
3
|
+
version: 0.6.4
|
|
4
4
|
scope: system
|
|
5
5
|
description: Proposes the branch's open GitHub pull request to a session, attached once you mention it (#N, the branch or its URL), and attaches any PR URL with chi context add or the web's "+ URL"; the agent gets a note when reviews, comments, checks or its state change; in the web, a path:line in an answer links to that line of the PR; the auto_attach setting can attach the PR at once instead, or do nothing
|
|
6
6
|
trust_level: reviewed
|
|
@@ -11,7 +11,7 @@ needs:
|
|
|
11
11
|
hint: brew install gh, then gh auth login
|
|
12
12
|
plugin:
|
|
13
13
|
file: plugin.rb
|
|
14
|
-
sha256: sha256:
|
|
14
|
+
sha256: sha256:87678c39565a3d586b462d0449a27ee3da4a96f53ef5b352dea959256f6c1eda
|
|
15
15
|
scripts:
|
|
16
16
|
pr_context.rb: sha256:a883d2780e89c68c0d7b6d5e0fcb22c273f17bb167e79064e0e99db6bd2ee6f0
|
|
17
17
|
context_providers:
|
|
@@ -29,11 +29,11 @@ require "samagotchi/git_env"
|
|
|
29
29
|
# diff" (or "File at head") action the web's ref menu offers. The
|
|
30
30
|
# after_turn hook links from a per-PR cache and never runs gh (the web holds the answer until the hooks are
|
|
31
31
|
# done); before_turn, before_generation and the init task fill the cache
|
|
32
|
-
# off the turn's thread (`gh api`), in a session worker only:
|
|
33
|
-
#
|
|
32
|
+
# off the turn's thread (`gh api`), in a session worker only: -p
|
|
33
|
+
# --non-interactive shows no web page while it runs (ctx.frontend; a chi
|
|
34
34
|
# without it counts as a worker), and a scratch session never fetches.
|
|
35
35
|
# `bundles: github-pr: line_links:` false turns it off, "always" fetches in
|
|
36
|
-
#
|
|
36
|
+
# -p --non-interactive too (still not for a scratch session).
|
|
37
37
|
#
|
|
38
38
|
# `bundles: github-pr: auto_attach:` says what the init task does with the
|
|
39
39
|
# branch's open PR: `propose` (the default, above), `attach` (attached at
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: inline-widgets
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
scope: system
|
|
5
|
+
description: "Teaches the model the chi-ui blocks chi web draws: picks (rows of options that fill your reply) and walk (a scenario stepped through)"
|
|
6
|
+
trust_level: reviewed
|
|
7
|
+
plugin:
|
|
8
|
+
file: plugin.rb
|
|
9
|
+
sha256: sha256:24ad0c3f0ffb592826b4a7389111d76723e9f28ea4d94ab63abda7986d3311bb
|
|
10
|
+
# chi.prompt_section is 0.132.0's; the web draws chi-ui blocks only from the next release
|
|
11
|
+
requires_chi: ">= 0.155.0"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The inline-widgets bundle (docs/bundles.md, The inline-widgets bundle): two
|
|
4
|
+
# prompt sections that teach the model the ```chi-ui blocks chi web draws
|
|
5
|
+
# (docs/internals/inline-widgets.md): picks, rows of options that fill the
|
|
6
|
+
# user's reply, and walk, a scenario stepped through. The drawing is core's; this
|
|
7
|
+
# bundle only says the blocks exist. Uninstalled, the prompt loses the sections
|
|
8
|
+
# and blocks already in a history still draw.
|
|
9
|
+
#
|
|
10
|
+
# The text is eval 0013's round-2 hint, split in two. It is static, so the
|
|
11
|
+
# prompt cache keeps: same text at every build. A delegate child's answer is
|
|
12
|
+
# read by its parent, not drawn for a user, and a one-shot (-p --non-interactive)
|
|
13
|
+
# prints to a terminal: neither gets a section.
|
|
14
|
+
class Plugin
|
|
15
|
+
PICKS = <<~TEXT
|
|
16
|
+
The web UI draws fenced ```chi-ui code blocks (YAML inside) as interactive widgets; the terminal shows their source. Use one whenever it helps the user more than prose: a widget replaces the long explanation, it doesn't sit beside one. Never use one for what markdown shows well (tables, lists, key-value). At most one widget per answer; keep the prose around it to a few sentences and don't repeat the widget's content.
|
|
17
|
+
|
|
18
|
+
picks: whenever your answer leaves the user two or more things to decide (review findings, a plan's open decisions, which of several fixes to do), even if they didn't ask you to list them. The user clicks one option per row, can add a note, and the UI fills their reply.
|
|
19
|
+
```chi-ui
|
|
20
|
+
type: picks
|
|
21
|
+
title: What to do with the findings
|
|
22
|
+
rows:
|
|
23
|
+
- id: "1" # unique, short; the user's reply names it
|
|
24
|
+
text: Bridge drops the replay buffer on reset # the item, <= 80 chars
|
|
25
|
+
options: # 2-4 short options, one per line
|
|
26
|
+
- fix now
|
|
27
|
+
- skip
|
|
28
|
+
- later
|
|
29
|
+
recommended: fix now # exactly one of the options
|
|
30
|
+
why: loses events a slow tab still needs # optional, one short line
|
|
31
|
+
```
|
|
32
|
+
Every row id unique; a row's options unique. Quote a value that starts with a quote or contains ": ".
|
|
33
|
+
TEXT
|
|
34
|
+
|
|
35
|
+
WALK = <<~TEXT
|
|
36
|
+
walk is the other ```chi-ui widget (the same rules: the web UI draws it, at most one widget per answer): a scenario the user steps through one step at a time: who does what, what the user sees, what happens behind it. Use it for "when does this happen / how does this work / what does the user go through" questions.
|
|
37
|
+
```chi-ui
|
|
38
|
+
type: walk
|
|
39
|
+
title: A tab reconnects
|
|
40
|
+
actors: [user, web, worker] # 2-5 short ids, unique
|
|
41
|
+
steps:
|
|
42
|
+
- id: s1 # unique
|
|
43
|
+
actor: user # one of actors
|
|
44
|
+
does: reopens the laptop # the action, <= 80 chars
|
|
45
|
+
sees: the page shows "reconnecting" # optional: what the user sees at this step
|
|
46
|
+
behind: the tab sends GET /events with Last-Event-ID # optional: what happens out of sight
|
|
47
|
+
- id: s2
|
|
48
|
+
actor: worker
|
|
49
|
+
does: checks the event id
|
|
50
|
+
cases: # optional: the step forks. List EVERY case, the usual one too
|
|
51
|
+
- when: the id is still buffered
|
|
52
|
+
goto: s3
|
|
53
|
+
- when: the id is too old
|
|
54
|
+
goto: s4
|
|
55
|
+
- id: s3
|
|
56
|
+
actor: worker
|
|
57
|
+
does: replays the missed events
|
|
58
|
+
next: end # optional: where to go after this step (default: the step below); end ends the walk here
|
|
59
|
+
- id: s4
|
|
60
|
+
actor: web
|
|
61
|
+
does: reloads the whole session
|
|
62
|
+
sees: the page redraws from the top
|
|
63
|
+
```
|
|
64
|
+
A step needs `does` or `sees` (or both). Steps run in order unless `next` or `cases` say otherwise. Every id unique; every goto names a step; a next names another step or is `end`. Quote a value that starts with a quote or contains ": ".
|
|
65
|
+
TEXT
|
|
66
|
+
|
|
67
|
+
def register(chi)
|
|
68
|
+
chi.prompt_section("picks") { |ctx| PICKS if drawn?(ctx) }
|
|
69
|
+
chi.prompt_section("walk") { |ctx| WALK if drawn?(ctx) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Whether the session's answers reach a web page that draws them.
|
|
75
|
+
def drawn?(ctx) = ctx.frontend != :one_shot && !ctx.delegate?
|
|
76
|
+
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skills
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.10
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Skills (skill_<name> memories, the steps of a task done together): /skill save, list, show and diff; older versions kept with a short diff line on every update; a nudge when a followed skill's step fails"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
plugin:
|
|
8
8
|
file: plugin.rb
|
|
9
|
-
sha256: sha256:
|
|
9
|
+
sha256: sha256:8d72d29286d0807c18d2c37225455ca06d45f4f974f9335b76cc77916043838c
|
|
10
10
|
requires_chi: ">= 0.143.0"
|
|
11
11
|
# config.yml bundles: skills: (another key warns; an older chi ignores this)
|
|
12
12
|
settings: [history_keep, nudge]
|
|
@@ -242,8 +242,9 @@ class Plugin
|
|
|
242
242
|
# what was just done (the model has seen the commands; a side answer
|
|
243
243
|
# wouldn't). The request runs as a turn; sent while a turn runs it joins
|
|
244
244
|
# that turn at its next step, and the request says to finish the task
|
|
245
|
-
# first. A
|
|
246
|
-
# shown for the user to
|
|
245
|
+
# first. A session a one-turn run holds (-p --non-interactive, or an older
|
|
246
|
+
# chi's terminal) takes no messages: the request is shown for the user to
|
|
247
|
+
# send.
|
|
247
248
|
def save(rest, ctx)
|
|
248
249
|
words = rest.split
|
|
249
250
|
scope = words.delete("--system") ? "system" : "project"
|
|
@@ -199,7 +199,7 @@ final class ChiRunner {
|
|
|
199
199
|
private static let recentFlags = ["--limit", "20"]
|
|
200
200
|
|
|
201
201
|
/// The live rows, then up to +recentCount+ of the recent list that are
|
|
202
|
-
/// neither live nor a
|
|
202
|
+
/// neither live nor held by an owner (a one-turn run); a failed recent list adds none.
|
|
203
203
|
private static func rows(live: [LiveSession], recent: Result<[LiveSession], ChiError>, recentCount: Int) -> [LiveSession] {
|
|
204
204
|
let liveIds = Set(live.map(\.id))
|
|
205
205
|
let stopped = ((try? recent.get()) ?? [])
|
|
@@ -2,137 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
module Samagotchi
|
|
4
4
|
class Engine
|
|
5
|
-
# The attached-context candidate judge (ContextJudge
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# The attached-context candidate judge (TurnStages::ContextJudge,
|
|
6
|
+
# forwarded): prepare_turn starts its pass, run_turn's ensure shows the
|
|
7
|
+
# cards it held, and a plugin's late candidate asks it. Reads
|
|
8
|
+
# @context_judge, @side_judgments, @host_registry.
|
|
9
9
|
module ContextJudgePart
|
|
10
10
|
private
|
|
11
11
|
|
|
12
|
-
# The
|
|
13
|
-
|
|
14
|
-
# are logged (ContextCandidatesLog#look) and, unless
|
|
15
|
-
# context.candidates.mode is off, the proposed candidates due an ask
|
|
16
|
-
# are asked about the owner's prompts so far, this one included. Never
|
|
17
|
-
# a participant's turn, a review session or a Continue (no new prompt).
|
|
18
|
-
def start_context_judge(turn)
|
|
19
|
-
session = turn.session
|
|
20
|
-
return unless turn.prompt.is_a?(String) && OwnerPrompt.turn?(session: session, author: turn.origin.author)
|
|
21
|
-
|
|
22
|
-
context_judge_pass(session, ContextCandidates.owner_texts(session.messages) + [turn.prompt], phase: "turn")
|
|
23
|
-
end
|
|
24
|
-
private :start_context_judge
|
|
12
|
+
# The judge's stage (TurnStages::ContextJudge).
|
|
13
|
+
attr_reader :context_judge
|
|
25
14
|
|
|
26
15
|
# A late proposal no owner prompt mentions (Plugin::Host
|
|
27
|
-
# #candidate_proposed:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def context_judge_pass(session, texts, phase:)
|
|
41
|
-
return if session.id.nil? || session.scratch
|
|
42
|
-
|
|
43
|
-
rows = ContextCandidates.with_status(session.id, project_root: session.project_root, state_dir: session_state_dir)
|
|
44
|
-
return if rows.empty?
|
|
45
|
-
|
|
46
|
-
log = ContextCandidatesLog.log_for(session_state_dir)
|
|
47
|
-
records = ContextCandidatesLog.records_for(log, session.id)
|
|
48
|
-
ContextCandidatesLog.look(log, session.id, rows, records) if phase == "turn"
|
|
49
|
-
settings = ContextJudge::Settings.from_config
|
|
50
|
-
return if settings.mode == "off"
|
|
51
|
-
|
|
52
|
-
jobs = ContextJudge.jobs(rows, records, session_id: session.id, texts: texts, settings: settings, phase: phase,
|
|
53
|
-
parent_attached: context_parent_attached(session))
|
|
54
|
-
return if jobs.empty?
|
|
55
|
-
|
|
56
|
-
client = system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :context),
|
|
57
|
-
ContextJudge::JUDGMENT, session, mode: settings.mode)
|
|
58
|
-
return unless client
|
|
59
|
-
|
|
60
|
-
start_context_asks(client, jobs, log, settings, context_choice(session, log, settings))
|
|
61
|
-
rescue StandardError => e
|
|
62
|
-
ContextJudge.warn(e)
|
|
63
|
-
end
|
|
64
|
-
private :context_judge_pass
|
|
65
|
-
|
|
66
|
-
# @return [Thread, nil] the asks' thread, nil when none started
|
|
67
|
-
def start_context_asks(client, jobs, log, settings, choice)
|
|
68
|
-
if @side_judgments.in_flight?(:context)
|
|
69
|
-
Log.info(:context, "context_judge_in_flight_skipped", session_id: jobs.first.session_id)
|
|
70
|
-
return nil
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
@side_judgments.start(:context, warn: ContextJudge.method(:warn)) do
|
|
74
|
-
jobs.each { |job| choice.call(job, ContextJudge.ask(client: client, job: job, log: log, settings: settings)) }
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
private :start_context_asks
|
|
78
|
-
|
|
79
|
-
# What an answer does on the asks' thread (ContextCandidateChoice
|
|
80
|
-
# #after_judged: on mode attaches or cards; audit nothing). Taken on
|
|
81
|
-
# the caller's thread. The attach only writes the session's folder: its
|
|
82
|
-
# note comes with the next between-turn delivery, never into a running
|
|
83
|
-
# turn. A card shows only while this Engine is still on that session,
|
|
84
|
-
# and not in a running turn (it isn't the turn's: no "waiting for you"
|
|
85
|
-
# on it), but once it has ended.
|
|
86
|
-
# @return [#call] (job, fields)
|
|
87
|
-
def context_choice(session, log, settings)
|
|
88
|
-
session_id = session.id
|
|
89
|
-
project_root = session.project_root
|
|
90
|
-
state_dir = session_state_dir
|
|
91
|
-
show = lambda do |**card|
|
|
92
|
-
current = @session
|
|
93
|
-
next nil unless current.nil? || current.id == session_id
|
|
94
|
-
|
|
95
|
-
@context_cards_mutex.synchronize do
|
|
96
|
-
next @context_cards_after_turn << card if turn_running?
|
|
97
|
-
|
|
98
|
-
show_card(**card)
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
lambda do |job, fields|
|
|
102
|
-
ContextCandidateChoice.after_judged(job: job, fields: fields, settings: settings, project_root: project_root,
|
|
103
|
-
state_dir: state_dir, log: log, show_card: show)
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
private :context_choice
|
|
107
|
-
|
|
108
|
-
# The cards #context_choice held while a turn ran (#run_turn's ensure,
|
|
109
|
-
# after the turn flag is released).
|
|
110
|
-
def show_context_cards_after_turn
|
|
111
|
-
cards = @context_cards_mutex.synchronize { @context_cards_after_turn.slice!(0..) }
|
|
112
|
-
cards.each { |card| show_card(**card) }
|
|
113
|
-
rescue StandardError => e
|
|
114
|
-
ContextJudge.warn(e)
|
|
115
|
-
end
|
|
116
|
-
private :show_context_cards_after_turn
|
|
117
|
-
|
|
118
|
-
# A delegate child's parent's sources, by title (else name): the
|
|
119
|
-
# judge's started_from. nil for any other session (a chi send --new
|
|
120
|
-
# session has no parent; a fork isn't a delegate).
|
|
121
|
-
def context_parent_attached(session)
|
|
122
|
-
return nil unless session.delegate? && session.parent_id
|
|
123
|
-
|
|
124
|
-
parent_root = if Session.exist?(session.parent_id, state_dir: session_state_dir)
|
|
125
|
-
Session.load(session.parent_id, state_dir: session_state_dir).project_root
|
|
126
|
-
else
|
|
127
|
-
session.project_root
|
|
128
|
-
end
|
|
129
|
-
ContextSources.attached(session.parent_id, project_root: parent_root, state_dir: session_state_dir)
|
|
130
|
-
.map { |attached| attached.source.title || attached.name }
|
|
131
|
-
rescue StandardError => e
|
|
132
|
-
Log.warn(:context, "context_parent_attached_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
133
|
-
[]
|
|
16
|
+
# #candidate_proposed): TurnStages::ContextJudge#late_candidates.
|
|
17
|
+
def judge_late_candidates = @context_judge.late_candidates
|
|
18
|
+
|
|
19
|
+
# The stage over this Engine's side asks and its session (built in
|
|
20
|
+
# #build_tools_and_plugins, before the plugins load: one may propose a
|
|
21
|
+
# candidate as it loads).
|
|
22
|
+
def build_context_judge
|
|
23
|
+
TurnStages::ContextJudge.new(
|
|
24
|
+
side_judgments: @side_judgments, host_registry: @host_registry, state_dir: -> { session_state_dir },
|
|
25
|
+
recording: ->(client, judgment, session, mode:) { system_one_recording(client, judgment, session, mode: mode) },
|
|
26
|
+
session: -> { session }, messages: -> { plugin_messages }, turn_running: -> { turn_running? },
|
|
27
|
+
show_card: ->(**card) { show_card(**card) }
|
|
28
|
+
)
|
|
134
29
|
end
|
|
135
|
-
private :context_parent_attached
|
|
136
30
|
end
|
|
137
31
|
end
|
|
138
32
|
end
|
|
@@ -76,11 +76,13 @@ module Samagotchi
|
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
# Ask the user to approve a call the gate voted `ask` on
|
|
79
|
-
# (GuardrailWiring#request_approval)
|
|
79
|
+
# (GuardrailWiring#request_approval), for the running turn (its
|
|
80
|
+
# author), else the last one. The gate's own asks come with their
|
|
81
|
+
# call's TurnContext instead.
|
|
80
82
|
# @param verdict [Guardrails::Verdict]
|
|
81
83
|
# @return [Guardrails::Verdict]
|
|
82
84
|
def request_approval(verdict)
|
|
83
|
-
@guardrail_wiring.request_approval(verdict)
|
|
85
|
+
@guardrail_wiring.request_approval(verdict, turn_context || last_turn_context)
|
|
84
86
|
end
|
|
85
87
|
end
|
|
86
88
|
end
|
|
@@ -46,7 +46,7 @@ module Samagotchi
|
|
|
46
46
|
from_cwd: note[:from_cwd], text: note[:text], context_source: note[:context_source],
|
|
47
47
|
created_at: note[:created_at], objectives: entry }.compact)
|
|
48
48
|
announce_objectives(session) if entry
|
|
49
|
-
|
|
49
|
+
@objectives_stage.note(session, note)
|
|
50
50
|
message
|
|
51
51
|
end
|
|
52
52
|
end
|
|
@@ -76,14 +76,17 @@ module Samagotchi
|
|
|
76
76
|
# register it on the kernel's llm_context_turn): the same array the loop
|
|
77
77
|
# appends to, not a copy, so read it on the turn's thread (the pending
|
|
78
78
|
# input drain, at an iteration boundary: TurnCheckpoint), or from another
|
|
79
|
-
# only as a dup, read only (/objectives). nil when no
|
|
80
|
-
#
|
|
81
|
-
# last turn's
|
|
79
|
+
# only as a dup, read only (/objectives). nil when no turn runs, and
|
|
80
|
+
# while the running turn's loop has yet to register its own
|
|
81
|
+
# (llm_context_turn is never cleared, and still holds the last turn's
|
|
82
|
+
# then: its turn_id tells them apart).
|
|
82
83
|
# @return [Array<Hash>, nil]
|
|
83
84
|
def live_conversation
|
|
84
|
-
|
|
85
|
+
running = turn_context
|
|
86
|
+
return nil unless running && @kernel.respond_to?(:llm_context_turn)
|
|
85
87
|
|
|
86
|
-
@kernel.llm_context_turn
|
|
88
|
+
forget = @kernel.llm_context_turn
|
|
89
|
+
forget&.turn_id == running.id ? forget.conversation : nil
|
|
87
90
|
end
|
|
88
91
|
|
|
89
92
|
# Append messages to the current session's conversation.
|