samagotchi 0.83.0 → 0.85.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 +31 -1
- data/bin/chi +27 -2
- data/docs/architecture.md +10 -1
- data/docs/cli.md +17 -1
- data/docs/configuration.md +2 -0
- data/docs/hooks.md +1 -1
- data/docs/internals/bundles.md +1 -1
- data/docs/internals/engine.md +3 -2
- data/docs/plugins.md +31 -5
- data/docs/sessions.md +15 -2
- data/docs/sub-agent.md +5 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -0
- data/lib/samagotchi/bridge.rb +54 -1
- data/lib/samagotchi/bridge_client.rb +12 -0
- data/lib/samagotchi/bundle_session_settings.rb +212 -0
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +4 -2
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +12 -5
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +1 -1
- data/lib/samagotchi/kernel_loop.rb +5 -1
- data/lib/samagotchi/llm/chat_loop.rb +5 -1
- data/lib/samagotchi/log_subscriber.rb +1 -0
- data/lib/samagotchi/memory_bundle/builder.rb +3 -2
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +6 -3
- data/lib/samagotchi/memory_bundle/installer.rb +2 -1
- data/lib/samagotchi/memory_bundle/manifest.rb +24 -2
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
- data/lib/samagotchi/parent_report.rb +5 -2
- data/lib/samagotchi/plugin/context.rb +36 -2
- data/lib/samagotchi/reply_wait.rb +8 -1
- data/lib/samagotchi/send_command.rb +24 -7
- data/lib/samagotchi/session.rb +16 -1
- data/lib/samagotchi/session_commands.rb +7 -4
- data/lib/samagotchi/session_inbox.rb +68 -0
- data/lib/samagotchi/session_setup.rb +14 -6
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +8 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +89 -1
- data/lib/samagotchi/terminal_ui.rb +10 -2
- data/lib/samagotchi/testing/bundle.rb +31 -5
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +41 -4
- data/lib/samagotchi/web/public/app.js +17 -0
- data/lib/samagotchi/web/public/bubbles.js +9 -0
- data/lib/samagotchi/web/public/commands.js +1 -0
- data/lib/samagotchi/web/public/data.js +11 -0
- data/lib/samagotchi/web/public/index.html +9 -0
- data/lib/samagotchi/web/public/stage_view.js +6 -0
- data/lib/samagotchi/web/public/stream_handlers.js +3 -0
- data/lib/samagotchi/web/public/turn_events.js +4 -0
- data/lib/samagotchi/web/public/turn_html.js +9 -1
- data/lib/samagotchi/web/public/unqueue.js +56 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d533709ed93dac5a1a65a9c023e83a50aef691542faffb1cdeefe6bfca51b5f1
|
|
4
|
+
data.tar.gz: 90cbb0c9256e751f64a36b854d6049098b694a8824a34230895e83eb383f8f74
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90149bcdee28e2c2aa74c011d361aee78b652f4a0ef03c966a2fa11324181b67b8a926e3a1fe94d6d7cc8b92e62edf5d7fc67f444ebec0923fe58f7338c3c65d
|
|
7
|
+
data.tar.gz: 8884e65ab2e6a0506e15dab8ea701dda33f3a149dc6168e770ad2b885dadf7789f0d4cf2df8c22a258239b74f3da8d384c356cd72db2c66ff8154eb31819acea
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,34 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.85.0] - 2026-10-18
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- A message queued for after the running turn can be taken back before it runs: the × on its bubble in the web,
|
|
16
|
+
`/unqueue` in an attached terminal (its newest queued line). Its text comes back into the input to change and send
|
|
17
|
+
again. Every UI drops it, and a `chi send --queue --wait` on it ends with `removed` (exit 1). `/queue` is now listed
|
|
18
|
+
in the attached terminal's Tab and `/help` too.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- On a phone, a queued message's text gets the bubble's full width, with its origin and after-turn badge above it.
|
|
23
|
+
- A plugin's `ctx.settings` reads a session's `--set` values again after a failed read, instead of keeping config.yml's
|
|
24
|
+
alone until the worker restarts.
|
|
25
|
+
- A `before_generation` hook now runs before the generation starts (`:generation_started`) in both loops: a question
|
|
26
|
+
it asks (the cost-cap's) no longer counts in the generation's time or leaves an empty generation on Stop.
|
|
27
|
+
|
|
28
|
+
Update with `chi update` (the system bundle 0.85.0), then restart `chi web`.
|
|
29
|
+
|
|
30
|
+
## [0.84.0] - 2026-10-18
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `--set BUNDLE.KEY=VALUE` on `chi` and `chi send --new`: a bundle setting for this session, saved with it and kept
|
|
35
|
+
by forks and continued links. A bundle lists the keys a session may set in its manifest's `session_settings:`;
|
|
36
|
+
its plugin reads them in `ctx.settings`, over config.yml's. `POST /api/sessions` takes `bundle_settings`.
|
|
37
|
+
- The cost-cap bundle takes a session's own cap: `chi send --new --set cost-cap.cap=3 -m "…"` (cost-cap 0.1.1).
|
|
38
|
+
|
|
11
39
|
## [0.83.0] - 2026-10-18
|
|
12
40
|
|
|
13
41
|
### Added
|
|
@@ -3380,7 +3408,9 @@ and long-lived sessions.
|
|
|
3380
3408
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3381
3409
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3382
3410
|
|
|
3383
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3411
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.85.0...HEAD
|
|
3412
|
+
[0.85.0]: https://github.com/dm1try/samagotchi/compare/v0.84.0...v0.85.0
|
|
3413
|
+
[0.84.0]: https://github.com/dm1try/samagotchi/compare/v0.83.0...v0.84.0
|
|
3384
3414
|
[0.83.0]: https://github.com/dm1try/samagotchi/compare/v0.82.0...v0.83.0
|
|
3385
3415
|
[0.82.0]: https://github.com/dm1try/samagotchi/compare/v0.81.0...v0.82.0
|
|
3386
3416
|
[0.81.0]: https://github.com/dm1try/samagotchi/compare/v0.80.0...v0.81.0
|
data/bin/chi
CHANGED
|
@@ -82,7 +82,7 @@ ARGV.each do |arg|
|
|
|
82
82
|
base = arg.split("=", 2).first
|
|
83
83
|
# If dashed version is a known flag (registry or hardcoded), reject underscore variant
|
|
84
84
|
dashed = base.tr("_", "-")
|
|
85
|
-
known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --toolkit --tool --no-tool --memory --mute --append-system-prompt --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
|
|
85
|
+
known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --toolkit --tool --no-tool --set --memory --mute --append-system-prompt --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
|
|
86
86
|
if known.include?(dashed) || known.include?(base)
|
|
87
87
|
warn "Unknown option: #{arg} (did you mean #{dashed}?)"
|
|
88
88
|
exit 1
|
|
@@ -125,6 +125,11 @@ parser = OptionParser.new do |opts|
|
|
|
125
125
|
# --no-tool is taken out of ARGV before parse! (OptionParser would read it
|
|
126
126
|
# as --tool's negation); this is its help line.
|
|
127
127
|
opts.separator " --no-tool NAME Leave this tool out of the toolkit (repeatable; a comma list too)"
|
|
128
|
+
opts.on("--set BUNDLE.KEY=VALUE", "A bundle setting for this session, over config.yml's bundles: (repeatable; a key the " \
|
|
129
|
+
"bundle's manifest lists in session_settings:; numbers, true, false and off are typed, the rest is text; " \
|
|
130
|
+
"saved in the session: --set cost-cap.cap=3)") do |v|
|
|
131
|
+
(options[:sets] ||= []) << v
|
|
132
|
+
end
|
|
128
133
|
opts.on("--memory NAME", "Preload a memory entry into the system prompt (repeatable; a comma list too)") { |m| (options[:memories] ||= []) << m }
|
|
129
134
|
opts.on("--append-system-prompt FILE", "Put FILE's text into the system prompt, after AGENTS.md (repeatable; saved in the session; " \
|
|
130
135
|
"config append_system_prompt: is the baseline)") { |f| (options[:append_system_prompt] ||= []) << f }
|
|
@@ -225,6 +230,10 @@ if options[:new_token] && !web
|
|
|
225
230
|
warn "Error: --new-token goes with chi web (chi web --new-token)"
|
|
226
231
|
exit 1
|
|
227
232
|
end
|
|
233
|
+
if options[:sets] && web
|
|
234
|
+
warn "Error: --set is a new session's (chi --set, chi send --new --set), not chi web's"
|
|
235
|
+
exit 2
|
|
236
|
+
end
|
|
228
237
|
|
|
229
238
|
# A scratch session is new, runs here and goes when it ends.
|
|
230
239
|
if scratch
|
|
@@ -372,6 +381,20 @@ if options[:toolkit] || options[:tools] || options[:no_tools]
|
|
|
372
381
|
remove: options[:no_tools])
|
|
373
382
|
end
|
|
374
383
|
|
|
384
|
+
# --set BUNDLE.KEY=VALUE: a new session's bundle settings, checked here
|
|
385
|
+
# against the installed bundles (a typo isn't saved).
|
|
386
|
+
if options[:sets]
|
|
387
|
+
require "samagotchi/bundle_session_settings"
|
|
388
|
+
begin
|
|
389
|
+
options[:bundle_settings] = Samagotchi::BundleSessionSettings.from_flags(options[:sets])
|
|
390
|
+
rescue Samagotchi::BundleSessionSettings::Error => e
|
|
391
|
+
warn "Error: #{e.message}"
|
|
392
|
+
exit 2
|
|
393
|
+
end
|
|
394
|
+
# An existing session's run names it among the flags it ignores.
|
|
395
|
+
options[:toolkit_flags] = (options[:toolkit_flags] || []) + ["--set"]
|
|
396
|
+
end
|
|
397
|
+
|
|
375
398
|
chi_update_hint.call
|
|
376
399
|
require "samagotchi/launch_mode"
|
|
377
400
|
launch, launch_note = if scratch
|
|
@@ -408,7 +431,8 @@ if launch == :attached
|
|
|
408
431
|
thinking: options[:thinking],
|
|
409
432
|
appended_prompt_files: options[:appended_prompt_files] || [],
|
|
410
433
|
toolkit: options[:toolkit_value],
|
|
411
|
-
toolkit_flags: options[:toolkit_flags] || []
|
|
434
|
+
toolkit_flags: options[:toolkit_flags] || [],
|
|
435
|
+
bundle_settings: options[:bundle_settings] || {})
|
|
412
436
|
rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
|
|
413
437
|
warn "Error: #{e.message}"
|
|
414
438
|
exit 1
|
|
@@ -430,6 +454,7 @@ begin
|
|
|
430
454
|
appended_prompt_files: options[:appended_prompt_files] || [],
|
|
431
455
|
toolkit: options[:toolkit_value],
|
|
432
456
|
toolkit_flags: options[:toolkit_flags] || [],
|
|
457
|
+
bundle_settings: options[:bundle_settings] || {},
|
|
433
458
|
non_interactive: options[:non_interactive],
|
|
434
459
|
model_name: options[:model],
|
|
435
460
|
scratch: scratch
|
data/docs/architecture.md
CHANGED
|
@@ -281,6 +281,15 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
281
281
|
saved messages, or the running turn's accumulator); answers once the
|
|
282
282
|
task is gone: `200 {status, stop_reason, task_id}`, `404 task_not_found`, `409 not_running`.
|
|
283
283
|
The worker names it in its sidecar features (`task_stop`).
|
|
284
|
+
- `POST /session/:id/turn/dequeue` — `{enqueued_id, client_id?}`: take back a queued message
|
|
285
|
+
(one that waits for the running turn's end: `--queue`, Alt+Enter, `/queue TEXT`, or a message
|
|
286
|
+
with images) before a turn takes it (web route `POST /api/sessions/:id/turn/:enqueued_id/dequeue`,
|
|
287
|
+
the × on a queued bubble; `/unqueue` in the attached TUI). Under the event log, like its enqueue:
|
|
288
|
+
`SessionInbox.take_queued_input` claims the file as the drain does (one rename wins), deletes it,
|
|
289
|
+
leaves a tombstone (`input/removed/<enqueued_id>`, for `ReplyWait`'s `turn:`), and the Bridge
|
|
290
|
+
announces `:turn_dequeued`. `200 {status: "removed", enqueued_id, prompt}`, `409 already_started`
|
|
291
|
+
(a turn took it), `409 not_queued` (a next-step message: a drain may merge it any moment), `404
|
|
292
|
+
unknown_enqueued_id`. It never wakes the worker. The worker names it in its sidecar features (`unqueue`).
|
|
284
293
|
- `POST /session/:id/relay` — the approval relay, on a delegated child's worker
|
|
285
294
|
(`{action, relay_id, question_id}`): `opened` / `closed` (with a `reason`) set or clear the
|
|
286
295
|
pending question's `relayed_to` mark (`QuestionDesk#annotate`, `:question_relay`); `answered`
|
|
@@ -404,7 +413,7 @@ session every 10 s, so a `kill -9` shows within a second. The summary (`Web::Ses
|
|
|
404
413
|
shared with `/api/sessions` and the session view) carries `owner`, `project_root` and
|
|
405
414
|
`bridge_up` (the sidecar is there *and* the lock is held: the page attaches its stream on it), and
|
|
406
415
|
from that sidecar `worker_version` (the chi the worker runs) and `worker_features`
|
|
407
|
-
(`Bridge::FEATURES`, what a client may ask of it beyond the routes: `restart`, `task_stop`, `reparent`,
|
|
416
|
+
(`Bridge::FEATURES`, what a client may ask of it beyond the routes: `restart`, `task_stop`, `reparent`, `unqueue`,
|
|
408
417
|
`delivery`, a worker that queues a message for after the turn; `SessionManager.deliver_turn` and
|
|
409
418
|
the attached TUI refuse a queue to a worker without it, which would steer; `turn_ids`, a worker whose running
|
|
410
419
|
turn's mark, continue turn and Stop carry the turn's `enqueued_id`, which `chi answer`, `chi send --wait` with no
|
data/docs/cli.md
CHANGED
|
@@ -204,6 +204,7 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
|
|
|
204
204
|
| `--model NAME` | Use this model for the run (overrides the configured default and a resumed session's model). |
|
|
205
205
|
| `--thinking LEVEL` | This session's own thinking level: `off`, `low`, `medium` or `high`, saved in the session and first in the order (`default` unsets it). On `--resume` or `--attach` it is set at once, through a `/thinking` on an attached worker. With `chi web` it is instead the default for the sessions its workers run, below a session's own (as env `SAMAGOTCHI_THINKING_LEVEL` is). See "Thinking" in configuration.md. |
|
|
206
206
|
| `--toolkit NAME`, `--tool NAME`, `--no-tool NAME` | A new session's toolkit, the tools its prompts declare: `full` (every built-in), `lean`, `child`, one of config's `toolkit.kits` or a bundle's (without `--toolkit`: config's `toolkit.default`), with tools declared on top of it (`--tool`) or left out (`--no-tool`); both repeatable, a comma list too. Saved in the session as given; a respawned worker, a fork and a continue keep it. Ignored with `--attach`/`--resume` (one line, as `--memory`); `/toolkit` changes it. Not on `chi web` (config's `toolkit.default` covers its sessions). See [Toolkits](configuration.md#toolkits). |
|
|
207
|
+
| `--set BUNDLE.KEY=VALUE` | A bundle setting for a new session (repeatable; a later one of the same key wins), laid over config.yml's `bundles: <bundle>:` in what the bundle's plugin reads (`ctx.settings`): `--set cost-cap.cap=3`. The value is typed simply: an integer (`3`, `-1`) or a decimal (`2.5`) is a number, `true` is true, `false` and `off` are false, anything else stays the text as given (`010`, `0x10`, `3:30`, `No`, `null`). For cost-cap, `0`, a negative number or `off` is no cap. Only a key the installed bundle's manifest lists in `session_settings:` is taken: a bundle that isn't installed (or whose plugin doesn't load), a key it doesn't list (a record installed before the bundle listed it says to run `chi update` or `chi bundle upgrade NAME`), or another shape (`--set foo`, `--set a.b.c=1`) is an error naming what it takes, exit 2, nothing started. Saved in the session; a respawned worker, a fork and a continue keep it, a delegate child doesn't. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. See [Per-session settings](plugins.md#per-session-settings). |
|
|
207
208
|
| `--profile NAME` | Prompt profile (`qwen36` or `gemma4`) for every model in this run, over config and the server's template (same as `--model-profile`, env `SAMAGOTCHI_MODEL_PROFILE`). See "Prompt profile" in configuration.md. |
|
|
208
209
|
| `--memory NAME` | Preload a memory entry into the system prompt (repeatable; a comma list too). Merged under the config.yml `memories:` baseline. Works attached: the list is stored on the session, so its worker builds the same prompt on every respawn. |
|
|
209
210
|
| `--append-system-prompt FILE` | Put `FILE`'s text into the system prompt, after AGENTS.md and before the memory indexes (repeatable; relative to the shell's directory). Added after the config.yml `append_system_prompt:` list. A missing or unreadable file is an error, exit 1. Saved in the session, so a respawned worker, a fork and a continue keep it; ignored with `--attach`/`--resume` (one line, as `--memory`). See [Appended prompt files](configuration.md#appended-prompt-files). |
|
|
@@ -606,6 +607,17 @@ REPL alike:
|
|
|
606
607
|
sent after it still go in at the next step.
|
|
607
608
|
At the open prompt (no turn running) `/cut TEXT` and `/queue TEXT` run TEXT
|
|
608
609
|
as a normal prompt.
|
|
610
|
+
- `/unqueue` (attached terminal) takes back the newest line this terminal
|
|
611
|
+
queued that hasn't started: `removed: <its first 60 characters>`, and its text
|
|
612
|
+
goes back into the input to change and send again (not over typed text: then
|
|
613
|
+
it is in the input history, ↑; not with input from a pipe; its images don't
|
|
614
|
+
come back), then the one before it on the next `/unqueue`; `(nothing queued: …)` when none waits,
|
|
615
|
+
`(not removed: it already started)` when its turn won the race, `(not removed:
|
|
616
|
+
it is already gone)` when another client removed it. Other terminals that
|
|
617
|
+
showed it say `(a queued message was removed)`; the web drops its bubble. A
|
|
618
|
+
worker older than it is not asked: `(/unqueue needs a newer worker: …)`. In
|
|
619
|
+
the plain REPL it answers with a note (its own queue is not touched), and the
|
|
620
|
+
web's composer points at the × on the bubble.
|
|
609
621
|
- **The web composer** has a row above the text field in a session (always
|
|
610
622
|
there, so the composer never changes height): the keys while idle, and while
|
|
611
623
|
the turn runs the delivery line, where a message sent now goes in (`↳ goes in after this
|
|
@@ -615,7 +627,11 @@ REPL alike:
|
|
|
615
627
|
**Alt+Enter** (Option+Enter) or `queue for after the turn` queues it, like
|
|
616
628
|
`/queue TEXT`, and the bubble is dashed (`after this turn`), below the
|
|
617
629
|
running turn's card with the commands queued for its end, until its turn
|
|
618
|
-
starts;
|
|
630
|
+
starts; its **×** (`Remove from queue`, once the worker acked it) takes it
|
|
631
|
+
back before it runs, in every tab (a toast says why when it can't: it
|
|
632
|
+
already started, it is gone, no live worker), and puts its text back into an
|
|
633
|
+
empty composer to change and send again (a draft stays: the text is then in
|
|
634
|
+
the toast; images don't come back); `cut in now` cuts, like `/cut TEXT` (by click only: it throws the
|
|
619
635
|
thinking away). Cut is enabled only while the step has streamed nothing but
|
|
620
636
|
thinking and cutting is on for the model (the line says `cuts at about 0:25
|
|
621
637
|
if you choose cut in now` until the thinking passes `steer_cut_after`, and
|
data/docs/configuration.md
CHANGED
|
@@ -1145,6 +1145,8 @@ Bridge acks `delivery: "queue"` while a turn runs (to an idle session the messag
|
|
|
1145
1145
|
`next_step`). A worker whose sidecar doesn't name the `delivery` feature (older than queueing) would steer it, so the
|
|
1146
1146
|
sender refuses it (`409 delivery_unavailable`, "restart it"), and so does a worker that answers the request with a
|
|
1147
1147
|
400 `delivery_unavailable`.
|
|
1148
|
+
Until its turn starts, a queued message can be taken back: `/unqueue` in an attached terminal, the × on its bubble
|
|
1149
|
+
in the web ([sessions.md](sessions.md), *Removing a queued message*).
|
|
1148
1150
|
|
|
1149
1151
|
## Server errors
|
|
1150
1152
|
|
data/docs/hooks.md
CHANGED
|
@@ -52,7 +52,7 @@ The plugin class must respond to `#call(event)` — duck-typed, no base class re
|
|
|
52
52
|
| `:session_start` | The first turn this chi process runs for the session (so again after `--resume`, a worker that idle-exited and woke, or a restart) | `{ type: :session_start, session_id: "..." }` |
|
|
53
53
|
| `:before_turn` | Before each turn starts | `{ type: :before_turn, session_id: "...", prompt: "..." (nil on a continue), resumed: true \| false (true on a step-limit Continue, which resumes the run before it; false on every other turn, a reminder's or a wake's too), messages: [...] (the history before this turn) }` |
|
|
54
54
|
| `:after_turn` | After a turn completed or was stopped once the model was asked (not after one that failed, was cut short by an interrupt signal (SIGINT), or was stopped before its `:before_turn` hooks) | `{ type: :after_turn, status: "completed" \| "canceled", present: (see [Presenting the answer](#presenting-the-answer-display-only)), refs: (see [Refs on the answer](#refs-on-the-answer-display-only)), messages: [...] (the conversation the turn stored; a cancelled or empty turn ends it with a `kind: turn_note` system message, and a context line is `kind: context`, see [sessions.md](sessions.md#notes-a-turn-leaves-for-the-model)) }` |
|
|
55
|
-
| `:before_generation` | Before each LLM API call (both loops) | `{ type: :before_generation, iteration: N }` |
|
|
55
|
+
| `:before_generation` | Before each LLM API call (both loops), before its `:generation_started`: the hook's time (a question it waits on) isn't the generation's | `{ type: :before_generation, iteration: N }` |
|
|
56
56
|
| `:after_generation` | After LLM returns (both loops); not after a generation a hook cut (`stop_generation`) | `{ type: :after_generation, iteration: N, response: "...", messages: [...] (the conversation as sent) }` |
|
|
57
57
|
| `:generation_progress` | While the response streams, in batches (see [Watching the stream](#watching-the-stream)) | `{ type: :generation_progress, iteration: N, thinking: "..." (new since the last fire), text: "..." (new visible text), thinking_chars: N, text_chars: N (this generation so far), elapsed_ms: N, restarted: true (only on the first fire after a dropped stream was asked again) }` |
|
|
58
58
|
| `:before_tool_call` | Before tool dispatch (and before `tool_call_started`) | `{ type: :before_tool_call, iteration: N, call: {...}, params: "...", guardrail: Verdict, context: {...}, targets: {...}, blocked: false, block_reason: nil }` |
|
data/docs/internals/bundles.md
CHANGED
|
@@ -27,7 +27,7 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
27
27
|
| `SourceIdentity` (source.rb) | what a source is, for comparing two installs of one name: `shipped:<dir>` (any gem or checkout), `git:<url>` (no `#ref`, trailing `/` or `.git`), `path:<realpath>`; nil when it can't place one |
|
|
28
28
|
| `PreparedSource` (source.rb) | one source normalized once (dir, owned, git commit): `BundleCommand` reads the bundle's name from its `manifest.yml` (the name for every kind of source, a git URL too) and hands it to the Installer (`prepared:`), so a source is cloned or extracted once per command |
|
|
29
29
|
| `MemoryFiles` | which top-level `*.md` files of a bundle are its memories (`.select`: the `files:` list, else all but README/LICENSE and the like), `.unshipped` (listed, not shipped) and `.symlinks` (skipped: a symlink is never a memory) |
|
|
30
|
-
| `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
|
|
30
|
+
| `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers, session_settings: the keys a session's `chi --set` may give, recorded at install in `manifest.json` for `BundleSessionSettings` to check) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
|
|
31
31
|
| `Installer` | one install/upgrade/dry run of one bundle (below) |
|
|
32
32
|
| `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps) |
|
|
33
33
|
| `Merger` | 3-way classification per file: `install`, `fast_forward`, `keep`, `noop`, `conflict` |
|
data/docs/internals/engine.md
CHANGED
|
@@ -179,7 +179,7 @@ KernelLoop#run(messages, max_iterations:, on_stream_event:, cancel_controller:,
|
|
|
179
179
|
│ 1. Queued input (user lines, plugin steers) goes in at the boundary (Steer.inject!)
|
|
180
180
|
│ 2. Format the conversation as prompt text (Prompt.format_with_images); a context
|
|
181
181
|
│ guidance line goes on the tail when the window fills past a threshold
|
|
182
|
-
│ 3.
|
|
182
|
+
│ 3. Fire :before_generation, then stream one generation (:generation_started, :generation_chunk, :generation_completed)
|
|
183
183
|
│ 4. Parse tool calls (ToolCallParser for the profile):
|
|
184
184
|
│ → Gemma 4: <|tool_call>call:NAME{params}<tool_call|>
|
|
185
185
|
│ → Qwen 3.6: <tool_call><function=NAME><parameter=KEY>VALUE</parameter></function></tool_call>
|
|
@@ -466,7 +466,8 @@ persistent observers (`Engine#subscribe`), which get a copy with a monotonic `ev
|
|
|
466
466
|
metrics, the debug log, the thinking tails, and in a worker the Bridge, which streams to the web and the attached
|
|
467
467
|
TUI. Errors in the sink or an observer are caught; they never break the turn.
|
|
468
468
|
|
|
469
|
-
`Engine#announce` puts an event that belongs to no turn (`ANNOUNCEABLE_EVENTS`: `turn_enqueued`, `
|
|
469
|
+
`Engine#announce` puts an event that belongs to no turn (`ANNOUNCEABLE_EVENTS`: `turn_enqueued`, `turn_dequeued`
|
|
470
|
+
(a queued message taken back before it ran, `{enqueued_id, client_id}`: the remover's), `card`,
|
|
470
471
|
`hook_notice`, `guardrail_warning`, `plugin_init_started`, and more) to the observers only.
|
|
471
472
|
|
|
472
473
|
Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
data/docs/plugins.md
CHANGED
|
@@ -28,6 +28,7 @@ plugin:
|
|
|
28
28
|
requires_chi: ">= 0.1.28" # optional: a gem-style requirement (">= 0.1.28, < 0.2"); the bundle's hooks/ check it too
|
|
29
29
|
# requires_chi_next: true # shipped bundles only, ignored by chi: rake release:bump raises requires_chi
|
|
30
30
|
# to the chi being released (docs/releasing.md)
|
|
31
|
+
session_settings: [cap] # optional: the settings a session may set (chi --set my-bundle.cap=3)
|
|
31
32
|
needs: [gh] # optional: outside commands it relies on (see memory.md#bundles-that-need-outside-commands)
|
|
32
33
|
```
|
|
33
34
|
|
|
@@ -70,7 +71,9 @@ of [anytime](#anytime-true) and the `save_note` tool (see `chi.tool` below). Ins
|
|
|
70
71
|
|
|
71
72
|
Settings work as they do for hooks ([hooks.md](hooks.md#settings)). An
|
|
72
73
|
`initialize` that takes an argument gets the bundle's section of config.yml
|
|
73
|
-
`bundles:`, as one Hash with string keys.
|
|
74
|
+
`bundles:`, as one Hash with string keys. It runs once, when the plugin loads, before any session: it never
|
|
75
|
+
sees a session's own `--set` values. A value a session may set is read from `ctx.settings` in the handler or
|
|
76
|
+
command that uses it (see [Per-session settings](#per-session-settings)).
|
|
74
77
|
|
|
75
78
|
## The API: `register(chi)`
|
|
76
79
|
|
|
@@ -731,7 +734,7 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
731
734
|
| `ctx.model` | the model the session runs on now: its resolved ref (`host:id`), right after `/model` too |
|
|
732
735
|
| `ctx.model_key` | that model's memory overlay key (`<name>.<key>.md`, what `memory_write current_model_only` writes) |
|
|
733
736
|
| `ctx.cost` | the session's spend so far, read now, as a frozen value: `total` (USD, reported + estimated, side requests included; 0.0 before any), `reported` (by providers, `usage.cost`), `estimated` (from `hosts.<name>.models` prices; a generation is one or the other, never both), `side` (recap, `/btw`, `ask_model`: their share of `total`) and `known?` (false while generations or side requests ran but none was reported or priced, so a limit on it can't be enforced; a session with some priced generations reads known, the unpriced ones counting 0). It includes the running turn's finished generations and what earlier workers of the session spent. Use its methods, not its class. Needs chi 0.83.0 |
|
|
734
|
-
| `ctx.settings` | the bundle's settings, frozen |
|
|
737
|
+
| `ctx.settings` | the bundle's settings, frozen: config.yml's `bundles: <bundle>:` with the session's own `--set` values laid over it ([Per-session settings](#per-session-settings)); the config alone until the session is bound (in `register` and `chi.ctx`) |
|
|
735
738
|
| `ctx.data_dir` | `$XDG_STATE_HOME/samagotchi/plugins/<bundle>/`, created on first use. A session's own state goes in `sessions/<id>.json` or `sessions/<id>/` there: chi removes it when the session is deleted, discarded or pruned |
|
|
736
739
|
| `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
|
|
737
740
|
| `ctx.messages` | the conversation, as a frozen copy, without the system prompt. While a turn runs, a session worker's (attached, web) adds that turn so far: its prompt, the model's text and the lines merged into it (no tool calls or thinking); the REPL's is the conversation before that turn |
|
|
@@ -754,6 +757,19 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
754
757
|
|
|
755
758
|
The Engine itself is never handed to a plugin.
|
|
756
759
|
|
|
760
|
+
### Per-session settings
|
|
761
|
+
|
|
762
|
+
A bundle's settings are config.yml's, for every session. A setting a session may have of its own (a child's cap,
|
|
763
|
+
say) is one the manifest lists in `session_settings:`; `chi --set BUNDLE.KEY=VALUE` and `chi send --new --set …`
|
|
764
|
+
then give it to a new session, saved with it (a respawn, a fork and a continued link keep it; a delegate child
|
|
765
|
+
doesn't). chi refuses a bundle that isn't installed (or whose plugin doesn't load) and a key the manifest doesn't
|
|
766
|
+
list, so a typo isn't saved. Values are typed simply: an integer (`3`, `-1`) or a decimal (`2.5`) is a number,
|
|
767
|
+
`true` is true, `false` and `off` are false, and anything else stays the text as given (`010`, `0x10`, `3:30`,
|
|
768
|
+
`No`, `null`); `POST /api/sessions` `bundle_settings` types its strings the same way and keeps JSON numbers and
|
|
769
|
+
booleans. `ctx.settings` is the config's section with the session's values laid over it (one level: a session's
|
|
770
|
+
`cap` replaces the config's `cap`). Read such a key from `ctx.settings` where it is used, not in
|
|
771
|
+
`initialize(settings)`: that runs once at load, before any session, and gets the config only.
|
|
772
|
+
|
|
757
773
|
## Cards
|
|
758
774
|
|
|
759
775
|
A card is a small framed message with buttons: a title, a body and
|
|
@@ -1147,6 +1163,7 @@ bundle.sessions.on(:children) { |all:| [] } # ctx.sessions, scripte
|
|
|
1147
1163
|
bundle.http.stub(:post, "http://127.0.0.1:9000/classify", json: { label: "safe" }) # ctx.http, never the network
|
|
1148
1164
|
bundle.set(running: false) # the session's values: cwd:, messages:, cancelled: …
|
|
1149
1165
|
bundle.set(cost: 2.5) # ctx.cost: reported USD, or { reported:, estimated:, side:, known: }
|
|
1166
|
+
bundle.set(session_settings: { "cap" => 1 }) # the session's --set values (Bundle.load takes session_settings: too)
|
|
1150
1167
|
bundle.shutdown
|
|
1151
1168
|
```
|
|
1152
1169
|
|
|
@@ -1173,6 +1190,11 @@ bundle.shutdown
|
|
|
1173
1190
|
- **`ctx.cost`** is zero and known unless the test sets it: `bundle.set(cost: 2.5)` (reported USD), a Hash of the
|
|
1174
1191
|
parts (`reported:`, `estimated:`, `side:`, `known:`; `total` is reported + estimated) or a callable giving either,
|
|
1175
1192
|
called at each read (a counting lambda shows whether the bundle read it).
|
|
1193
|
+
- **`session_settings:`** (on `Bundle.load` and `bundle.set`) are the session's own `--set` values for the bundle,
|
|
1194
|
+
keys its manifest lists in `session_settings:` (another raises `ArgumentError`, as chi refuses it). `ctx.settings`
|
|
1195
|
+
lays them over `settings:` through chi's own `Context#settings`; the plugin's `initialize(settings)` gets
|
|
1196
|
+
`settings:` only. `bundle.set(session_settings:)` moves the session to a new id (`test-session-2`, or the
|
|
1197
|
+
`session_id:` given beside it): chi never changes them within a session, and builds `ctx.settings` once per id.
|
|
1176
1198
|
- **A hook that raises fails the call** (`Testing::HookFailed`, with the hook's error) where chi would log it and
|
|
1177
1199
|
go on; `bundle.lenient { … }` collects them in `bundle.errors` instead.
|
|
1178
1200
|
- **Sandbox.** A test runs with temp config, state, data and cache homes (config.yml, memories, `ctx.data_dir`, the
|
|
@@ -1574,8 +1596,10 @@ bundles:
|
|
|
1574
1596
|
Continue raises it to $6.00.` with **Continue** (another cap's worth: $3 → $6 → $9, as many steps as it takes to
|
|
1575
1597
|
pass the spend, saved for the session) and **Stop** (the turn stops, `canceled` with `stopped_by: cost-cap`, and a
|
|
1576
1598
|
card shows the spend, reported, estimated and side). With no one to ask (`--non-interactive`) it stops.
|
|
1577
|
-
- **The cap of a session:** the one `/cap` or a Continue set for it, else `
|
|
1578
|
-
`
|
|
1599
|
+
- **The cap of a session:** the one `/cap` or a Continue set for it, else its own `cap:` (`chi --set
|
|
1600
|
+
cost-cap.cap=3`, `chi send --new --set cost-cap.cap=3`; `--set cost-cap.cap=off`, `0` or a negative number for none), else `child:` for a
|
|
1601
|
+
delegate child, else `default:`. Delegates and continued links each have their own cap (a continued link keeps
|
|
1602
|
+
the `--set` cap, not a `/cap` or Continue); there is no family cap.
|
|
1579
1603
|
- **A host it can't watch:** a model that reports no cost and has no configured price (a local server, a free
|
|
1580
1604
|
model that reports 0) reads unknown: one notice, `the $3.00 cap can't be enforced: <model> reports no cost and has
|
|
1581
1605
|
no price`, and it never stops for an unknown cost. A session with some priced generations counts the unpriced
|
|
@@ -1589,7 +1613,9 @@ bundles:
|
|
|
1589
1613
|
spend is allowed: the next generation asks.
|
|
1590
1614
|
- `/cap off` (or `/cap 0`): no cap for this session, over the config's `default:`.
|
|
1591
1615
|
|
|
1592
|
-
A parent agent starts a session with a cap
|
|
1616
|
+
A parent agent starts a session with a cap of its own with `--set` (`chi send --new --set cost-cap.cap=3 -m "task"`;
|
|
1617
|
+
`cost-cap.cap` is the bundle's one session setting), or by sending `/cap N` as a first message
|
|
1618
|
+
(`chi send --new -m '/cap 3'`).
|
|
1593
1619
|
At the cap, `chi send --wait` exits 3 with a question of kind `hook` (header "Cost cap"); `chi answer ID --question
|
|
1594
1620
|
Q --option Continue` (or `--option 1`) goes on, `--option Stop` stops the turn ([sub-agent.md](sub-agent.md)). A
|
|
1595
1621
|
parent may Continue: nothing restricts it, as with the step limit. A background delegate child at its cap waits on
|
data/docs/sessions.md
CHANGED
|
@@ -4,6 +4,8 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
4
4
|
|
|
5
5
|
**Input files:** a message for a session's worker waits in `input/<timestamp>.json` (`SessionInbox.write_input`: `prompt`, `client_id`, `enqueued_id`, and `images`, `no_interrupt` and `delivery` when they apply; `delivery` is written only for `"queue"`). While a turn runs, the worker merges them at each step boundary in name order, except a queued one or one with images: those wait for the turn's end and run one turn each, in name order, and a file after them still merges (`Worker#pending_input_drain`). A queue is sent only to a worker whose `bridge.json` names the `delivery` feature; an older one would steer it, so `SessionManager.deliver_turn` refuses it (409 `delivery_unavailable`), and any other 4xx the Bridge answers is a refusal too, never a fallback file (except a 413, a body over its size limit: that one still goes in as a file, its `delivery` kept). **Reply files:** a turn that ends with visible text leaves its reply in `output/<timestamp>.txt`, or `output/<timestamp>-<enqueued_id>.txt` when the turn ran an input file with an id (`SessionInbox.write_output`), and the session's `last_turn` keeps that `enqueued_id`; so a wait for one queued message (`chi send --queue --wait`, `ReplyWait`'s `turn:`) finds its own turn's reply or end, never another turn's. The names still sort by time, and every reader takes `*.txt`.
|
|
6
6
|
|
|
7
|
+
**Removing a queued message:** a message queued for after the running turn (`chi send --queue`, the web's Alt+Enter, `/queue TEXT` in a terminal), or one with images (it waits for the turn's end too), can be taken back while it still waits: the worker's Bridge (`POST /session/:id/turn/dequeue {enqueued_id}`, the `unqueue` feature) deletes its input file and every UI drops it (`:turn_dequeued`). It is a race with the turn's end, decided by the same rename the worker uses to take the file: once a turn took it the answer is `409 already_started` (it runs), and an id the worker no longer knows is `404 unknown_enqueued_id`. A plain message sent without `--queue` is not removable (`409 not_queued`): a running turn's next step may take it at any moment. A wait for its turn (`chi send --queue --wait`, a delegate follow-up's wait) ends at once with `removed` (the tombstone `input/removed/<enqueued_id>` says so), and a `chi -p` run whose queued prompt went ends with exit 1. The `200` carries the removed `prompt`: the web and the attached terminal put it back into the composer to edit (not its images). The images a removed message named stay in the session's `images/`. Not removable: a queued session command (`/queue /model x`: it waits in the worker, not in `input/`), a message in a stopped worker's inbox (the next worker runs it), and the plain REPL's (`--no-shared`) own queue.
|
|
8
|
+
|
|
7
9
|
**Retention (file-based, opt-out via env):**
|
|
8
10
|
|
|
9
11
|
| Env | Default | Purpose |
|
|
@@ -207,9 +209,10 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
|
|
|
207
209
|
- `--new` prints `<full id> started` on stdout. With `--wait` that line (and the `sent` line for an existing session) goes to stderr, so stdout is exactly the answer: the reply of the turn the message started, in full. It is the worker's `output/` file, the same one `delegate` reads; a turn that ends with only tool calls and no text has none.
|
|
208
210
|
- `--wait` takes one session: `--new` or one id. A session with a running turn is refused (`busy: a turn is running; wait or attach`, exit 1): the message would merge into it, and its reply would come back as the answer. `--queue --wait` is the way in: the message runs as a turn of its own after the running one, and the wait is for that turn alone (the ack's `enqueued_id` names its reply file and its `last_turn`), so the running turn's reply and end, and a question it asks meanwhile, are not the answer (it keeps waiting through them; its `--timeout` still applies); a question its own turn asks is (exit 3). Two queued waits each get their own turn's answer. A session waiting for an answer is still refused. `--format json` adds `delivery`: `queue`, or `next_step` when the running turn ended before the message went in (it then ran at once, and the answer is still its own).
|
|
209
211
|
- Exit codes with `--wait`: 0 answered; 3 the turn waits for an answer from you (a question, a hook's question, a guardrail approval, or the step-limit question: see [chi as a sub-agent](sub-agent.md#the-step-limit)), and the session keeps waiting: stderr gets the whole question (`waiting for an answer (approval): …`, its text, the numbered options, whether free text is allowed) and how to answer it (`chi answer ID --question QID --option N`, or `chi --attach ID`, the web); 4 `--timeout S` passed with the turn still running (it goes on; wait again); 1 the turn ended without a reply (canceled, failed or empty), the worker failed or vanished, or the session was stopped; 130 on Ctrl-C, which leaves the turn running (`still running: chi --attach ID`). There is no default timeout.
|
|
210
|
-
- `--format json` (with `--wait`) prints one JSON object on stdout however the wait ends, for a script or a parent agent (see [chi as a sub-agent](sub-agent.md)): `{"status":"answered","session_id":…,"text":…}`; `{"status":"question",…,"question":{"id","kind","header","text","options","multi_select","allow_freeform","approval"},"answer_with":"chi answer ID --question QID --option N"}`, where `kind` is `question` (the model's), `hook` or `approval` (with the tool, command, cwd, rule, reason and offered scopes); `{"status":"running",…,"detail":…}` (exit 4, and on Ctrl-C); and `failed`, `canceled`, `no_answer`, `error`, `worker_gone` or `stopped` with a `detail` line (exit 1); `failed` adds `error_kind` and `retryable`, `canceled` adds `cancel_reason` and `stopped_by`, when known. A failure before the wait (no such session, a busy one) is `error` too. A usage error (exit 2) prints no JSON.
|
|
212
|
+
- `--format json` (with `--wait`) prints one JSON object on stdout however the wait ends, for a script or a parent agent (see [chi as a sub-agent](sub-agent.md)): `{"status":"answered","session_id":…,"text":…}`; `{"status":"question",…,"question":{"id","kind","header","text","options","multi_select","allow_freeform","approval"},"answer_with":"chi answer ID --question QID --option N"}`, where `kind` is `question` (the model's), `hook` or `approval` (with the tool, command, cwd, rule, reason and offered scopes); `{"status":"running",…,"detail":…}` (exit 4, and on Ctrl-C); and `failed`, `canceled`, `removed` (a `--queue --wait` message taken back before it ran), `no_answer`, `error`, `worker_gone` or `stopped` with a `detail` line (exit 1); `failed` adds `error_kind` and `retryable`, `canceled` adds `cancel_reason` and `stopped_by`, when known. A failure before the wait (no such session, a busy one) is `error` too. A usage error (exit 2) prints no JSON.
|
|
211
213
|
- `--wait ID` with no message (no `-m`, nothing piped) sends nothing: it waits for the session's next reply and prints and exits as above. With a turn running, that turn's reply (its `interrupted_turn` mark names it), not a turn's queued behind it; with the step-limit question standing, the reply of the turn its Continue runs; with an idle session whose last turn already ended, that ended turn is the answer: its reply, or its end when it left no reply — a turn that finished in the gap between waits is reported, not waited for. With none of those (or a worker whose `bridge.json` lacks `turn_ids`), the next reply of any turn. A running session is fine (that is the point), so after exit 3, 4 or 130 an agent waits again this way while the user answers in the web or `chi --attach`; the question pending when it starts doesn't count again. An idle session with no worker waits until something wakes one. With `--new`, or two ids, it is a usage error (exit 2).
|
|
212
|
-
- `--
|
|
214
|
+
- `--set BUNDLE.KEY=VALUE` (with `--new`, repeatable) gives the new session a bundle setting of its own, as `chi --set` does (see *Bundle settings* below): `chi send --new --set cost-cap.cap=3 -m "…"` starts it with a $3 cap. A bundle or key the installed bundles don't offer is a usage error (exit 2), as is `--set` without `--new`.
|
|
215
|
+
- `--continues ID` (with `--new`) starts the next link of that session's chain instead (see *Session chains*): in its folder, on its model (or the one `--model` names), LLM context, thinking level, toolkit and bundle settings, so `--dir`, `--llm-context*`, `--thinking`, `--toolkit`, `--tool`, `--no-tool` and `--set` are refused beside it; `ID` may be a prefix or `last:ID`, the chain's latest link. Without a message the new link starts idle; `--wait` then has nothing to wait for (a usage error). It prints `<full id> started (continues 3fa2c1d0)`, with `; 2 delegates moved` inside the parentheses when open delegates moved to it; a refusal (continued already, naming the next link; an open delegate that can't move; a turn running) is `chi send: refused: …`, exit 1, and a `--model` it can't start on is `chi send: could not start a session: …`, exit 1, nothing archived. A fallback to the default model (below) prints `chi send: warning: …`.
|
|
213
216
|
- The 16 KiB cap applies: `git diff | chi send --new …` on a big diff is refused; name the branch in the message instead and let the session read it.
|
|
214
217
|
- A session nobody attaches to stalls at its first question or guardrail ask until someone answers it (`chi answer`, the web, `chi --attach`), and its worker stays up meanwhile: it doesn't idle-exit while a question waits (`chi sessions list` shows it as `waiting`; `chi sessions stop ID` frees it). Otherwise it idle-exits after `session.idle_exit_minutes` like any worker. These are ordinary sessions: delete them like any other.
|
|
215
218
|
|
|
@@ -243,6 +246,16 @@ declares every tool (full). The file stores only the name: a kit's tools and pro
|
|
|
243
246
|
`/toolkit` changes it, saved at once, from the next turn's start. See
|
|
244
247
|
[Toolkits](configuration.md#toolkits).
|
|
245
248
|
|
|
249
|
+
## Bundle settings
|
|
250
|
+
|
|
251
|
+
A session file's `bundle_settings` (`{"cost-cap": {"cap": 3}}`, `{}` for none and for a session from before them)
|
|
252
|
+
holds the bundle settings it was created with: `chi --set BUNDLE.KEY=VALUE`, `chi send --new --set …`, or `POST
|
|
253
|
+
/api/sessions` `bundle_settings` (an object of the same shape, checked the same way; `400 invalid_bundle_settings`
|
|
254
|
+
otherwise). They never change after: a respawned or woken worker reads the same, and a continue (`continue →`,
|
|
255
|
+
`chi send --new --continues`) and a fork (`ctx.sessions.fork`) copy them, as they copy the toolkit. A delegate
|
|
256
|
+
child starts without (a bundle has its own setting for children, such as cost-cap's `child:`). A bundle's plugin
|
|
257
|
+
reads them in `ctx.settings`, over config.yml's `bundles:` ([Per-session settings](plugins.md#per-session-settings)).
|
|
258
|
+
|
|
246
259
|
## Delegating
|
|
247
260
|
|
|
248
261
|
The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail above 64 KB (`DelegateWait.cut`); a reply longer than `max_tool_output_chars` keeps its start and its end (the conclusion), with a gap line naming that `output/` file, where the whole reply is (`ToolOutputCut`'s `:head_tail_ref`; see [Tool output guardrails](internals/tool-guardrails.md#the-runners-cut-max_tool_output_chars)). Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (folded into its parent's card on the strip and in all sessions, whose `▸ N delegates` chip lists the delegates at any depth, see Web UI above; a card of its own with a `↳` chip when its parent isn't listed; `delegated by` in its info bar; the parent's info bar carries a `⑂ N` chip counting its delegates, with how many run, wait, are done, failed, stopped or idle in the tooltip and the waiting ones named in the attention colour; a click opens all sessions), the terminal's status row (a parent's `⑂ 2 running · 1 waiting`, see [Status Line](cli.md#status-line)) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
|
data/docs/sub-agent.md
CHANGED
|
@@ -36,6 +36,10 @@ chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--f
|
|
|
36
36
|
chi answer ID --question QID --dismiss [--timeout S] [--format json]
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
A child with a bundle setting of its own, such as a $ cap (the [cost-cap bundle](plugins.md#the-cost-cap-bundle)):
|
|
40
|
+
`chi send --new --wait --format json --set cost-cap.cap=3 -m "task"`. At the cap the wait exits 3 with the cost-cap
|
|
41
|
+
question; `chi answer ID --question QID --option Continue` (or `Stop`) answers it.
|
|
42
|
+
|
|
39
43
|
`chi answer` waits after answering, the same way `chi send --wait` does, so the
|
|
40
44
|
next question comes back as exit 3 again. What it waits for is the answered
|
|
41
45
|
turn: the turn that asked (or, for the step-limit question, the turn it
|
|
@@ -55,7 +59,7 @@ Exit codes, both commands:
|
|
|
55
59
|
| 0 | `not_continued` | (`chi answer`) a Stop answered the step-limit question; the turn's work so far stays |
|
|
56
60
|
| 3 | `question` | chi waits for an answer: `question`, `answer_with` |
|
|
57
61
|
| 4 | `running` | `--timeout` passed; the turn goes on, wait again |
|
|
58
|
-
| 1 | `failed`, `canceled`, `limit`, `no_answer`, `error`, `worker_gone`, `stopped` | `detail` says what happened (`limit`: the turn ran out of steps and nobody can be asked, e.g. an older worker; `chi send ID -m '/continue yes'` continues it). `failed` adds `error_kind` (the provider error's kind: `credits`, `credits_held`, `rate_limited`, `server`, `auth`, `connection`, …) and `retryable` when known; `canceled` adds `cancel_reason` (`user`: a Stop in the web; `session_stopped`: a session stop, `chi sessions stop` or the web's; `hook`; `ctrl_c`; `manual`) and `stopped_by` (the hook that stopped it, e.g. `loop-guard`) when known |
|
|
62
|
+
| 1 | `failed`, `canceled`, `limit`, `removed`, `no_answer`, `error`, `worker_gone`, `stopped` | `detail` says what happened (`limit`: the turn ran out of steps and nobody can be asked, e.g. an older worker; `chi send ID -m '/continue yes'` continues it; `removed`: the `--queue` message waited for was taken back before it ran, no turn ran for it). `failed` adds `error_kind` (the provider error's kind: `credits`, `credits_held`, `rate_limited`, `server`, `auth`, `connection`, …) and `retryable` when known; `canceled` adds `cancel_reason` (`user`: a Stop in the web; `session_stopped`: a session stop, `chi sessions stop` or the web's; `hook`; `ctrl_c`; `manual`) and `stopped_by` (the hook that stopped it, e.g. `loop-guard`) when known |
|
|
59
63
|
| 2 | (no JSON) | usage error, or an option the question doesn't offer |
|
|
60
64
|
| 130 | `running` | Ctrl-C; the turn goes on |
|
|
61
65
|
|
|
@@ -232,6 +232,9 @@ module Samagotchi
|
|
|
232
232
|
queued[:images] = event[:images] if event[:images]
|
|
233
233
|
queued[:delivery] = event[:delivery] if event[:delivery]
|
|
234
234
|
@queued << queued
|
|
235
|
+
# Taken back before it ran (Bridge#handle_dequeue_turn).
|
|
236
|
+
when :turn_dequeued
|
|
237
|
+
dequeue([{ enqueued_id: event[:enqueued_id] }])
|
|
235
238
|
when :recap_ready
|
|
236
239
|
# One collected just as a turn started describes the chat before it.
|
|
237
240
|
@recap = event[:recap] unless @turn
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -517,7 +517,10 @@ module Samagotchi
|
|
|
517
517
|
# turn's end (Delivery::QUEUE_FEATURE). A worker without it would steer.
|
|
518
518
|
# turn_ids: the running turn's checkpoint mark, a continue turn and a
|
|
519
519
|
# Stop's last_turn carry the turn's enqueued_id (ReplyWait.own_turn).
|
|
520
|
-
|
|
520
|
+
# unqueue: POST turn/dequeue (#handle_dequeue_turn) takes a queued
|
|
521
|
+
# message back before it runs.
|
|
522
|
+
FEATURES = ["restart", "task_stop", "reparent", Delivery::QUEUE_FEATURE, ReplyWait::TURN_IDS_FEATURE,
|
|
523
|
+
SessionInbox::UNQUEUE_FEATURE].freeze
|
|
521
524
|
|
|
522
525
|
# The routes, by method and what follows /session/:id/: a handler takes
|
|
523
526
|
# the session id and the request body and returns [headers, status, body].
|
|
@@ -528,6 +531,7 @@ module Samagotchi
|
|
|
528
531
|
%w[POST answer] => :handle_answer,
|
|
529
532
|
%w[POST question/dismiss] => :handle_dismiss_question,
|
|
530
533
|
%w[POST turn] => :handle_post_turn,
|
|
534
|
+
%w[POST turn/dequeue] => :handle_dequeue_turn,
|
|
531
535
|
%w[POST command] => :handle_command,
|
|
532
536
|
%w[POST exit] => :handle_exit_request,
|
|
533
537
|
%w[POST recap] => :handle_recap,
|
|
@@ -932,6 +936,55 @@ module Samagotchi
|
|
|
932
936
|
[{}, 202, ack]
|
|
933
937
|
end
|
|
934
938
|
|
|
939
|
+
# POST /session/:id/turn/dequeue {enqueued_id, client_id?, deadline?}:
|
|
940
|
+
# take back a queued message (one that waits for the running turn's
|
|
941
|
+
# end: a queue or an image message) before a turn takes it. With the
|
|
942
|
+
# event log held, as its enqueue was, so :turn_dequeued always follows
|
|
943
|
+
# its :turn_enqueued; the file is claimed as the drain claims it (one
|
|
944
|
+
# rename wins), deleted, and its tombstone left for a wait on its turn
|
|
945
|
+
# (SessionInbox.take_queued_input). It never wakes the worker.
|
|
946
|
+
# 200 {status: "removed", enqueued_id, prompt}; 409 already_started (a
|
|
947
|
+
# turn took it, or the snapshot still lists it with no file left: taken,
|
|
948
|
+
# not announced yet), 409 not_queued (a next-step message: a drain may
|
|
949
|
+
# merge it any moment); 404 unknown_enqueued_id (never not_found: that is
|
|
950
|
+
# the unrouted path's, what a worker older than the route answers);
|
|
951
|
+
# 408 past its +deadline+ (see #handle_post_turn), nothing removed.
|
|
952
|
+
def handle_dequeue_turn(session_id, body)
|
|
953
|
+
parsed = parse_json(body)
|
|
954
|
+
return [{ "Allow" => "POST" }, 400, { error: "invalid_json" }] unless parsed.is_a?(Hash)
|
|
955
|
+
|
|
956
|
+
id = fetched(parsed, "enqueued_id").to_s.strip
|
|
957
|
+
return [{ "Allow" => "POST" }, 400, { error: "missing_fields", detail: "enqueued_id required" }] if id.empty?
|
|
958
|
+
|
|
959
|
+
deadline = fetched(parsed, "deadline")
|
|
960
|
+
return BAD_DEADLINE unless deadline_valid?(deadline)
|
|
961
|
+
|
|
962
|
+
client_id = fetched(parsed, "client_id")
|
|
963
|
+
taken, input = @engine.synchronize_events do
|
|
964
|
+
raise IntakeClosed if stopped?
|
|
965
|
+
next [:expired, nil] if expired?("dequeue_expired", deadline, sid: session_id, enqueued_id: id)
|
|
966
|
+
|
|
967
|
+
SessionInbox.take_queued_input(session_dir, id).tap do |status, _|
|
|
968
|
+
@engine.announce(type: :turn_dequeued, enqueued_id: id, client_id: client_id) if status == :removed
|
|
969
|
+
end
|
|
970
|
+
end
|
|
971
|
+
dequeue_reply(taken, id, input)
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def dequeue_reply(taken, id, input)
|
|
975
|
+
case taken
|
|
976
|
+
when :removed then [{}, 200, { status: "removed", enqueued_id: id, prompt: input.prompt.to_s, session_id: @session_id }]
|
|
977
|
+
when :expired then deadline_passed("removal")
|
|
978
|
+
when :not_queued
|
|
979
|
+
[{}, 409, { error: "not_queued", detail: "only a message queued for after the turn can be removed", enqueued_id: id }]
|
|
980
|
+
when :claimed then [{}, 409, { error: "already_started", enqueued_id: id }]
|
|
981
|
+
else
|
|
982
|
+
return [{}, 409, { error: "already_started", enqueued_id: id }] if @accumulator.queued.any? { |q| q[:enqueued_id] == id }
|
|
983
|
+
|
|
984
|
+
[{}, 404, { error: "unknown_enqueued_id", enqueued_id: id }]
|
|
985
|
+
end
|
|
986
|
+
end
|
|
987
|
+
|
|
935
988
|
# Read-only snapshot surface. AC #4: too-old reconnects re-derive state
|
|
936
989
|
# from here.
|
|
937
990
|
def handle_state(_session_id, _body = nil)
|
|
@@ -132,6 +132,18 @@ module Samagotchi
|
|
|
132
132
|
post("question/dismiss", { id: id, deadline: deadline }, read_body: true)
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
+
# POST /session/:id/turn/dequeue: take back a queued message (one that
|
|
136
|
+
# waits for the running turn's end) before it runs. 200 {status:
|
|
137
|
+
# "removed", enqueued_id, prompt}; 409 already_started (a turn took
|
|
138
|
+
# it) or not_queued (a next-step message); 404 unknown_enqueued_id; a
|
|
139
|
+
# 404 not_found: a worker older than the route (its sidecar lacks
|
|
140
|
+
# "unqueue"); 408 = too late.
|
|
141
|
+
# @param client_id [String, nil] the removing UI, in its :turn_dequeued
|
|
142
|
+
# @return [Response]
|
|
143
|
+
def dequeue(enqueued_id:, client_id: nil)
|
|
144
|
+
post("turn/dequeue", { enqueued_id: enqueued_id, client_id: client_id, deadline: deadline }, read_body: true)
|
|
145
|
+
end
|
|
146
|
+
|
|
135
147
|
# POST /session/:id/turn. 202 = queued (body carries the enqueued_id the
|
|
136
148
|
# Bridge also announced in :turn_enqueued), 408 deadline_passed = the
|
|
137
149
|
# Bridge read it too late and dropped it (see DEADLINE_SHARE).
|