samagotchi 0.9.0 → 0.10.1
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 +68 -1
- data/docs/architecture.md +24 -9
- data/docs/cli.md +4 -3
- data/docs/guardrails.md +5 -3
- data/docs/hooks.md +44 -7
- data/docs/internals/gemma4-contract.md +1 -0
- data/docs/plugins.md +36 -13
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bootstrap_command.rb +29 -29
- data/lib/samagotchi/bridge/card_store.rb +4 -4
- data/lib/samagotchi/bridge/pending_card.rb +2 -2
- data/lib/samagotchi/bridge/sse_writer.rb +22 -28
- data/lib/samagotchi/bridge/turn_accumulator.rb +92 -5
- data/lib/samagotchi/bridge.rb +59 -112
- data/lib/samagotchi/bridge_client.rb +11 -28
- data/lib/samagotchi/bundle_command.rb +158 -212
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +3 -4
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +13 -4
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +10 -10
- data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
- data/lib/samagotchi/bundles/mcp/plugin.rb +2 -4
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +18 -9
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +0 -1
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cli/command.rb +51 -0
- data/lib/samagotchi/cli/flags.rb +116 -0
- data/lib/samagotchi/config.rb +70 -55
- data/lib/samagotchi/config_text_edit.rb +107 -0
- data/lib/samagotchi/desktop_command.rb +24 -18
- data/lib/samagotchi/edit_preview.rb +2 -2
- data/lib/samagotchi/engine.rb +20 -75
- data/lib/samagotchi/events.rb +23 -0
- data/lib/samagotchi/guardrail_wiring.rb +2 -5
- data/lib/samagotchi/hooks/bundle_loader.rb +13 -30
- data/lib/samagotchi/hooks/loader.rb +23 -33
- data/lib/samagotchi/hooks/registry.rb +41 -0
- data/lib/samagotchi/hooks.rb +0 -3
- data/lib/samagotchi/host_registry.rb +15 -0
- data/lib/samagotchi/idle_reminders.rb +0 -7
- data/lib/samagotchi/image_store.rb +22 -0
- data/lib/samagotchi/kernel_loop.rb +12 -16
- data/lib/samagotchi/live_versions.rb +2 -13
- data/lib/samagotchi/llm/native_tool_normalizer.rb +10 -227
- data/lib/samagotchi/memory_bundle/builder.rb +1 -14
- data/lib/samagotchi/memory_bundle/index_updater.rb +2 -28
- data/lib/samagotchi/memory_bundle/installer.rb +1 -27
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -6
- data/lib/samagotchi/memory_bundle/status.rb +1 -14
- data/lib/samagotchi/memory_bundle/system_bundle.rb +2 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +1 -24
- data/lib/samagotchi/memory_paths.rb +10 -0
- data/lib/samagotchi/note_command.rb +23 -37
- data/lib/samagotchi/owner_lock.rb +14 -3
- data/lib/samagotchi/plugin/api.rb +11 -17
- data/lib/samagotchi/plugin/context.rb +70 -15
- data/lib/samagotchi/plugin/loader.rb +2 -3
- data/lib/samagotchi/sampling_settings.rb +3 -5
- data/lib/samagotchi/self_report.rb +6 -9
- data/lib/samagotchi/send_command.rb +33 -52
- data/lib/samagotchi/session.rb +88 -92
- data/lib/samagotchi/session_archive_command.rb +8 -14
- data/lib/samagotchi/session_commands.rb +3 -21
- data/lib/samagotchi/session_delete_command.rb +15 -29
- data/lib/samagotchi/session_manager.rb +27 -13
- data/lib/samagotchi/terminal_ui/attached_loop.rb +30 -80
- data/lib/samagotchi/terminal_ui/attached_view.rb +1 -10
- data/lib/samagotchi/terminal_ui/between_turns.rb +133 -0
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +51 -1
- data/lib/samagotchi/terminal_ui/status_row.rb +32 -0
- data/lib/samagotchi/terminal_ui/thinking_line.rb +3 -3
- data/lib/samagotchi/terminal_ui.rb +156 -234
- data/lib/samagotchi/thinking.rb +2 -3
- data/lib/samagotchi/thought_stream_splitter.rb +10 -9
- data/lib/samagotchi/tool_call_parser.rb +56 -292
- data/lib/samagotchi/tools/builtin_calls.rb +146 -0
- data/lib/samagotchi/tools/builtins.rb +2 -1
- data/lib/samagotchi/tools/edit.rb +21 -29
- data/lib/samagotchi/tools/memory.rb +1 -11
- data/lib/samagotchi/tools/send_note.rb +1 -1
- data/lib/samagotchi/update_command.rb +32 -22
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +4 -5
- data/lib/samagotchi/web/app.rb +89 -151
- data/lib/samagotchi/web/public/activity.js +1 -11
- data/lib/samagotchi/web/public/annotate_bar.js +120 -0
- data/lib/samagotchi/web/public/app.js +158 -785
- data/lib/samagotchi/web/public/chunk_router.js +7 -6
- data/lib/samagotchi/web/public/command_list.js +99 -0
- data/lib/samagotchi/web/public/data.js +22 -66
- data/lib/samagotchi/web/public/format.js +0 -23
- data/lib/samagotchi/web/public/index.html +0 -2
- data/lib/samagotchi/web/public/model_pick.js +0 -7
- data/lib/samagotchi/web/public/notifications.js +169 -0
- data/lib/samagotchi/web/public/question_cards.js +352 -0
- data/lib/samagotchi/web/public/session_state.js +34 -0
- data/lib/samagotchi/web/public/timing.js +1 -1
- data/lib/samagotchi/web/public/turn_events.js +11 -2
- data/lib/samagotchi/web/public/turn_model.js +0 -14
- data/lib/samagotchi/web/session_hub.rb +6 -4
- data/lib/samagotchi/web/session_summary.rb +5 -3
- data/lib/samagotchi/worker.rb +59 -53
- data/lib/samagotchi/worker_sidecar.rb +27 -1
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7fbd9765613c81776d1a8d62e1edacd50cc426843dcae07d2c0e6a11ebe80972
|
|
4
|
+
data.tar.gz: 8ed4b10801de4650ded6cd223d7723055acd8d0fa1ed3e98af36fe2cb174c403
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd72e187dab11b52fa7ba403eeecd3ea29b43b2e2364b31cce773279cbbadb18c0539e30f16fed5e76ae849f82c31e76e300cb2f6a419699c127c7396cc14263
|
|
7
|
+
data.tar.gz: 4abd84f3c1a83d0afa57239709919c9753c4eb8037503532e5a373a6ef3d0e674cfee330d15a717c50c48b11e5adf2be6611178c13aaf26fa4237fb89ed030e4
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,71 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.10.1] - 2026-10-01
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- The `known-names` bundle no longer rejects a shell glob of a known name, such as `ls -d myrepo*` in a repo named
|
|
16
|
+
`myrepo`: a token with `*?[]{}` is not checked.
|
|
17
|
+
- The `skills` bundle no longer counts a call that a guardrail or the user denied as a failed skill step, so it
|
|
18
|
+
doesn't steer the model to fix the skill or say "a step failed" at the turn's end.
|
|
19
|
+
|
|
20
|
+
Update with `chi update` (bundles: known-names 0.1.3, skills 0.1.4).
|
|
21
|
+
|
|
22
|
+
## [0.10.0] - 2026-10-01
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Built-in tool calls are built the same way for every model format (Gemma and Qwen native, OpenAI-style chat), so
|
|
27
|
+
they no longer differ by format. As part of that:
|
|
28
|
+
- `edit` takes the old and new text as two fields (`old_text`, `new_text`), so old text that contains `</old>` is
|
|
29
|
+
no longer cut short; `edit` without `new_text` is an error (it deleted the matched text; pass `""` to delete).
|
|
30
|
+
- `write` without `content` is an error with every model format (Gemma and Qwen emptied the file).
|
|
31
|
+
- `register_reminder` from a Qwen model without an interval no longer always fails.
|
|
32
|
+
- Plugins: `ctx.notify`, `ctx.ask_user`, `ctx.steer`, `ctx.stop_turn` and `ctx.stop_generation` are the one way a plugin
|
|
33
|
+
acts; inside a `chi.on` block they act for that event as `event[:x]` does (`ctx.stop_turn` in `:before_tool_call`
|
|
34
|
+
denies the call, `ctx.steer` from `:after_turn` is false), and as before anywhere else (commands, your own threads).
|
|
35
|
+
Plain hook files keep `event[:x]`.
|
|
36
|
+
- Attaching to a session mid-turn (`chi --attach`) shows the turn's finished tool calls as they looked live, with
|
|
37
|
+
what each did and how long it took (`tool> running command (execute …): ok (1.3s)`), when the worker runs this chi.
|
|
38
|
+
- `chi bundle` usage errors (unknown subcommand or flag, a missing argument) exit 2 like every other command, not 1.
|
|
39
|
+
- A config hook with `on_error: log` warns as `[samagotchi:hooks] <file> (config) failed: <error>`, the same shape as a
|
|
40
|
+
bundle hook's warning; a fail-closed bundle guardrail's deny reason reads `... (bundle <name>) raised <error>`.
|
|
41
|
+
- Bundles (`chi update`):
|
|
42
|
+
- known-names 0.1.2: corrects a near-miss name only in paths, `cwd` and commands, no longer inside text being
|
|
43
|
+
written to a file (`write`/`memory_write` content, `edit`'s old and new text, where a correction could stop the
|
|
44
|
+
edit matching).
|
|
45
|
+
- loop-guard 0.2.2: tells repeated `edit` calls apart by their old and new text; acts through `ctx`.
|
|
46
|
+
- skills 0.1.3 and mcp 0.3.2 (mcp now needs chi 0.8.0): write a skill's history and the MCP tool cache through a
|
|
47
|
+
unique temporary file, so two workers saving at once no longer collide.
|
|
48
|
+
- check-in 0.1.3 acts through `ctx`; source-links 0.3.1 drops dead code. They behave as before.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- Web: a Gemma model on a native llama.cpp host streams its answer into the reply as it writes (it showed only when the
|
|
53
|
+
turn ended), with its thoughts in the thinking block; the attached terminal shows them as thinking and the answer as
|
|
54
|
+
writing.
|
|
55
|
+
- `execute` with a `cwd` now runs in that directory with Gemma and Qwen native tool calls too (the directory was
|
|
56
|
+
dropped and the command ran in the project root).
|
|
57
|
+
- `/model … --default` and `/model … --alias` keep the comments and layout of `config.yml`: they change one line
|
|
58
|
+
(or add one) instead of rewriting the whole file.
|
|
59
|
+
- `chi sessions stop` marks the session with a `stopped` file in its folder instead of rewriting its session file, so
|
|
60
|
+
a worker saving at that moment can no longer undo the stop (or lose its own save); a resume removes the file.
|
|
61
|
+
- The web's answer, dismiss, command and cancel calls fail the same way: a worker that doesn't answer in time is
|
|
62
|
+
504 ("did not answer, so ... was not ..."; cancel said "not running"), a worker running an older chi is 501 with
|
|
63
|
+
how to restart it (an answer said "not running").
|
|
64
|
+
- A web page joining (or re-syncing with) a live session gets its status and pending question as of the same moment
|
|
65
|
+
as the messages; they could be a step newer.
|
|
66
|
+
- The web's `POST /api/sessions/:id/turn` refuses more than 20 images in one message, as a live worker already did.
|
|
67
|
+
- Web: switching sessions no longer briefly shows the previous session's "delegated by" link in the info bar.
|
|
68
|
+
- The terminal's "retrying (1/3 …)" line counts retries as the web does (it said "1/4", counting the first try).
|
|
69
|
+
- `chi self`'s thinking line finds a `models:` entry under the model an alias points at, as a turn does.
|
|
70
|
+
- A `required: true` config hook that raises something other than a StandardError (e.g. `NotImplementedError`) denies
|
|
71
|
+
the tool call like any other raise, instead of escaping the guardrail check.
|
|
72
|
+
|
|
73
|
+
Update with `chi update` (bundles: known-names 0.1.2, loop-guard 0.2.2, skills 0.1.3, mcp 0.3.2, check-in 0.1.3,
|
|
74
|
+
source-links 0.3.1).
|
|
75
|
+
|
|
11
76
|
## [0.9.0] - 2026-10-01
|
|
12
77
|
|
|
13
78
|
### Changed
|
|
@@ -513,7 +578,9 @@ and long-lived sessions.
|
|
|
513
578
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
514
579
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
515
580
|
|
|
516
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
581
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.10.1...HEAD
|
|
582
|
+
[0.10.1]: https://github.com/dm1try/samagotchi/compare/v0.10.0...v0.10.1
|
|
583
|
+
[0.10.0]: https://github.com/dm1try/samagotchi/compare/v0.9.0...v0.10.0
|
|
517
584
|
[0.9.0]: https://github.com/dm1try/samagotchi/compare/v0.8.1...v0.9.0
|
|
518
585
|
[0.8.1]: https://github.com/dm1try/samagotchi/compare/v0.8.0...v0.8.1
|
|
519
586
|
[0.8.0]: https://github.com/dm1try/samagotchi/compare/v0.7.0...v0.8.0
|
data/docs/architecture.md
CHANGED
|
@@ -115,9 +115,9 @@ bin/chi ─▶ TerminalUI ─▶ Engine#run_turn ─▶ KernelLoop ──┬─
|
|
|
115
115
|
|
|
116
116
|
## Session retention & ordering
|
|
117
117
|
|
|
118
|
-
- **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|owner.lock|bridge.json` (XDG-aware).
|
|
118
|
+
- **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|owner.lock|bridge.json` (XDG-aware), and the `<uuid>/stopped` and `<uuid>/archived` marker files.
|
|
119
119
|
- **Single owner:** the process running a session's Engine (worker or in-process TUI) holds a flock on `owner.lock` (`OwnerLock`); a second owner backs off, and the web answers 409 for a TUI-owned session.
|
|
120
|
-
- **Status:** `status` is turn state (`idle`/`running`); liveness is the lock.
|
|
120
|
+
- **Status:** `status` is turn state (`idle`/`running`); liveness is the lock. A stop is the `<uuid>/stopped` marker (`Session.mark_stopped`; a resume removes it), which `Session.load` and `.summary_from_file` read as status `stopped`, so no other process rewrites a worker's session file.
|
|
121
121
|
- **Retention:** 14 days / 500 cap (env `SAMAGOTCHI_SESSION_RETENTION_DAYS`/`MAX_COUNT`, optional `KEEP_STATUS`), live-owner guard, only when `*.json` present; lazy sweep ≤1/24h from the session hub's full-probe tick (and on `GET /api/sessions`, which the page no longer calls) & `Dashboard#render_list`, manual via `bin/chi sessions prune --dry-run`.
|
|
122
122
|
- **Ordering:** `Session.list(sort:,order:,limit:,offset:)` and `GET /api/sessions?sort=&order=&limit=&offset=` default `updated_at desc`; Web UI sort/filter/pagination.
|
|
123
123
|
- **Test hygiene:** `test_run` flag when `SAMAGOTCHI_ENV=test`/`RACK_ENV=test`/`CI`, targetable via `prune --test-only` / `clean`.
|
|
@@ -208,7 +208,8 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
208
208
|
`?client_id=` names whose stream it is (`Bridge#open_streams_except`, used by `POST /exit`).
|
|
209
209
|
- `POST /session/:id/turn` — fire-and-forget turn creation; returns `202` with an `enqueued_id`
|
|
210
210
|
(delivery is at-least-once via the worker's file-IPC input path — it never calls `run_turn`
|
|
211
|
-
across the HTTP boundary).
|
|
211
|
+
across the HTTP boundary). Only the bridge's own session: another id, in the path or the body's
|
|
212
|
+
`session_id`, is `404 unknown_session`. Inspect results through the read surface, not the turn response.
|
|
212
213
|
An optional `deadline` (epoch seconds; `BridgeClient` sends 5/6 of its read timeout ahead) makes
|
|
213
214
|
a request read after it (a worker frozen by sleep or SIGSTOP) answer `408 deadline_passed` and
|
|
214
215
|
not run: a client that timed out has said the message was not sent. `/answer`,
|
|
@@ -216,6 +217,10 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
216
217
|
log held, as a turn is), and the Bridge logs `turn_expired`, `answer_expired`, `dismiss_expired`
|
|
217
218
|
or `command_expired`. The web app answers either kind of timeout with `504 worker_timeout`
|
|
218
219
|
("… so the command was not run"). `/cancel`, `/recap` and `/exit` take none.
|
|
220
|
+
The web relays answer, dismiss, command and cancel the same way (`App#relay`): a 408 or a read
|
|
221
|
+
timeout is `504 worker_timeout`; a Bridge 404 for a route an older worker lacks is
|
|
222
|
+
`501 not_supported` with the restart hint; a Bridge `404 unknown_session`, no bridge or a
|
|
223
|
+
refused connection is `503 not_live`.
|
|
219
224
|
- `POST /session/:id/cancel` — cancel the running turn; `202`, or `409` when none runs.
|
|
220
225
|
- `POST /session/:id/answer` — answer the pending question; `200`, `409` when another client
|
|
221
226
|
answered first or it is gone, `400` for an invalid selection.
|
|
@@ -235,6 +240,10 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
235
240
|
the messages itself, then streams from its `event_seq`).
|
|
236
241
|
- `OPTIONS *` — CORS preflight (`Access-Control-Allow-Origin: *`).
|
|
237
242
|
|
|
243
|
+
Every route goes through `Bridge#dispatch`: an id other than the bridge's own session is
|
|
244
|
+
`404 unknown_session`, and a handler that raises answers `500 bridge_error` (logged as
|
|
245
|
+
`handler_failed`) rather than dropping the connection.
|
|
246
|
+
|
|
238
247
|
The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
|
|
239
248
|
for client discovery. `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
|
|
240
249
|
(503 `not_live` when the worker is not running; full history of any session is served by
|
|
@@ -248,15 +257,15 @@ changes to every open tab over `GET /api/events` (SSE: a `snapshot` frame on eve
|
|
|
248
257
|
then `session` for an upsert and `session_gone` for a removal, `: ping` while idle, no replay).
|
|
249
258
|
Files stay the source of truth and workers don't know the hub. Its watcher is a 1 s tick that
|
|
250
259
|
stats the sessions dir (every session writer goes tmp + rename, which bumps the dir's mtime) and
|
|
251
|
-
each `<id>/` folder (recap.json, bridge.json), re-parsing only the files whose mtime or size
|
|
260
|
+
each `<id>/` folder (recap.json, bridge.json, the stopped and archived markers), re-parsing only the files whose mtime or size
|
|
252
261
|
moved through `Session.summary_from_file`. Liveness is probed, since a killed owner leaves no
|
|
253
262
|
file trace: the owner lock every tick for the sessions the projection believes owned, and every
|
|
254
263
|
session every 10 s, so a `kill -9` shows within a second. The summary (`Web::SessionSummary`,
|
|
255
264
|
shared with `/api/sessions` and the session view) carries `owner`, `project_root` and
|
|
256
265
|
`bridge_up` (the sidecar is there *and* the lock is held: the page attaches its stream on it).
|
|
257
266
|
`POST/DELETE /api/sessions` and `/stop` rescan the session before answering (`SessionHub#touch`).
|
|
258
|
-
|
|
259
|
-
|
|
267
|
+
`GET /api/sessions` is the hub's projection too (its sort, paging and total). `Server` always builds
|
|
268
|
+
a hub; an `App.new` without one answers both routes `503 no_hub`.
|
|
260
269
|
|
|
261
270
|
## Model loops and adapters
|
|
262
271
|
|
|
@@ -269,9 +278,15 @@ Engine picks the loop from the effective model's host (`HostRegistry#resolve`):
|
|
|
269
278
|
|
|
270
279
|
Both return an `LLM::ModelResult` and emit the same stream events; tool calls in
|
|
271
280
|
both go through `ToolRunner` (events, hooks, veto, output cap) and
|
|
272
|
-
`KernelLoop#dispatch_tool_call`.
|
|
273
|
-
`
|
|
274
|
-
|
|
281
|
+
`KernelLoop#dispatch_tool_call`. Every format's built-in calls (Gemma, Qwen,
|
|
282
|
+
chat `tool_calls`) are built by one table, `Tools::BuiltinCalls`, from the
|
|
283
|
+
parsed `{name, args}` (derived from `TOOL_SCHEMAS`), so a call looks the same to
|
|
284
|
+
hooks and tools whichever model made it. Both loops' `generation_chunk` carries
|
|
285
|
+
`thinking:`, `text:` and `content:` (both): the chat loop's thinking is the
|
|
286
|
+
server's `reasoning_content`; the raw-prompt loop splits each generation's
|
|
287
|
+
stream with a fresh `ThoughtStreamSplitter` (Qwen's `<think>` and Gemma's
|
|
288
|
+
`<|channel>thought … <channel|>` are thinking, tool-call bodies are dropped
|
|
289
|
+
from `text:`). The chat loop's model turns keep their `tool_calls` and tool results their `tool_call_id`,
|
|
275
290
|
so later requests and resumed sessions pair them. It has its own system prompt
|
|
276
291
|
(no raw-prompt tool declarations; the tools go as JSON schemas).
|
|
277
292
|
|
data/docs/cli.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
- `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text to live sessions as context notes (see [Desktop helper](desktop.md))
|
|
22
22
|
- `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles; `chi self --model` prints only the model a new session starts on (the desktop helper's hint)
|
|
23
23
|
- `chi update [--dry-run] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
|
|
24
|
-
- `chi bundle install|upgrade|uninstall|status|diff|list|build` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Plugins](plugins.md#the-btw-bundle), [the mcp bundle](plugins.md#the-mcp-bundle) [the loop-guard bundle](plugins.md#the-loop-guard-bundle), [the check-in bundle](plugins.md#the-check-in-bundle) and [the skills bundle](plugins.md#the-skills-bundle))
|
|
24
|
+
- `chi bundle install|upgrade|uninstall|status|diff|list|build` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Plugins](plugins.md#the-btw-bundle), [the mcp bundle](plugins.md#the-mcp-bundle) [the loop-guard bundle](plugins.md#the-loop-guard-bundle), [the check-in bundle](plugins.md#the-check-in-bundle) and [the skills bundle](plugins.md#the-skills-bundle)). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for every other command
|
|
25
25
|
|
|
26
26
|
### First setup
|
|
27
27
|
|
|
@@ -586,9 +586,10 @@ In interactive assist mode, you can switch the request model without restarting:
|
|
|
586
586
|
|
|
587
587
|
Notes:
|
|
588
588
|
|
|
589
|
-
- The switch updates the request `model` field, routes to the matching host (`HostRegistry
|
|
589
|
+
- The switch updates the request `model` field, routes to the matching host (`HostRegistry#resolve`), and resolves the prompt profile again (config, the server's chat template, the name; see "Prompt profile" in configuration.md).
|
|
590
590
|
- Without `--default` the command is session-scoped and does not rewrite config files.
|
|
591
591
|
- With `--default` the new default is written to `~/.config/samagotchi/config.yml` (honoring `XDG_CONFIG_HOME`) and takes effect for all new sessions; the current session's effective model is also updated immediately. Bare aliases and `host:model` are both valid.
|
|
592
|
+
- `--default` and `--alias` change only their one key's line in `config.yml` (`default.model`, or the alias under `model_aliases:`): comments and the rest of the file stay as written, a symlinked file is written through and keeps its mode. When the edited text wouldn't read back as the expected settings (a flow-style section, anchors), chi writes the whole file out from the parsed data instead, which drops its comments.
|
|
592
593
|
- Worker sessions inherit `hosts:` via `SAMAGOTCHI_HOSTS_JSON`.
|
|
593
594
|
- The idle recap uses the session's current model (a switch counts from the next recap), unless `recap: {host_ref, model}` pins one.
|
|
594
595
|
|
|
@@ -706,7 +707,7 @@ While a turn runs, one row above the prompt says what it is doing, the spinner f
|
|
|
706
707
|
Qwen `TURN:` prefix and inline markdown are left out.
|
|
707
708
|
- `| waiting for the first token… 5s` after 2 s with nothing streamed.
|
|
708
709
|
- `| running execute…` while a tool runs (its `tool>` line prints when it ends).
|
|
709
|
-
- `| retrying (1/
|
|
710
|
+
- `| retrying (1/3 in 0.5s): Errno::ECONNREFUSED` while a network error is retried.
|
|
710
711
|
- `| mcp: starting servers…` while a plugin's slow setup (an init task) runs, between turns too;
|
|
711
712
|
`mcp> ✓ …` prints when it is done.
|
|
712
713
|
|
data/docs/guardrails.md
CHANGED
|
@@ -222,7 +222,8 @@ local part, the repo folder name, and any names from config. A token in a
|
|
|
222
222
|
call's command, `cwd` or paths (never a write's content) that is within one
|
|
223
223
|
edit of a known name shorter than 10 characters, or two edits of a longer
|
|
224
224
|
one, is a near miss. Tokens and names shorter than `min_length` (6) are
|
|
225
|
-
skipped, as is a token that equals another known name.
|
|
225
|
+
skipped, as is a token that equals another known name. A token with a shell
|
|
226
|
+
glob (`*?[]{}`, as in `ls -d samagotchi*`) is not checked.
|
|
226
227
|
|
|
227
228
|
By default (`mode: reject`) the call is denied with advice in place of the
|
|
228
229
|
usual tail, so the model retries it corrected:
|
|
@@ -244,8 +245,9 @@ bundles:
|
|
|
244
245
|
max_distance: 2 # default: 1 under 10 characters, else 2
|
|
245
246
|
```
|
|
246
247
|
|
|
247
|
-
`mode: correct` rewrites the call (whole tokens, everywhere they appear
|
|
248
|
-
|
|
248
|
+
`mode: correct` rewrites the call (whole tokens, everywhere they appear in
|
|
249
|
+
the command, the `cwd` and the paths; never in file text: a write's content
|
|
250
|
+
or an edit's `old_text`/`new_text`) and says so; `mode: ask` shows the call with three choices, *Correct it and
|
|
249
251
|
run*, *Run as is*, *Deny*; with no one to ask (`--non-interactive`) or a
|
|
250
252
|
dismissed question it rejects. A real near name (a folder `jondoe` next to
|
|
251
253
|
user `johndoe`, a login one letter from another) is caught too: list it under
|
data/docs/hooks.md
CHANGED
|
@@ -140,10 +140,10 @@ whole response), and none after the turn or the generation was cancelled.
|
|
|
140
140
|
A 240k-char thinking gives about 120 fires.
|
|
141
141
|
|
|
142
142
|
`thinking` holds what the model streamed as thinking: `reasoning_content`
|
|
143
|
-
on the chat path, a Qwen `<think>` block
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
answer's content. Tool-call bodies are left out of `text`. With streaming off
|
|
143
|
+
on the chat path, a Qwen `<think>` block or a Gemma 4
|
|
144
|
+
`<|channel>thought … <channel|>` block on the raw-prompt path. Some
|
|
145
|
+
thinking arrives as `text` instead: a chat provider's that puts `<think>`
|
|
146
|
+
inside the answer's content. Tool-call bodies are left out of `text`. With streaming off
|
|
147
147
|
(`stream: false` on a chat host) there are no chunks, so no fires.
|
|
148
148
|
|
|
149
149
|
**Keep it fast.** The hook runs on the turn's thread, inside the HTTP read:
|
|
@@ -368,6 +368,43 @@ end
|
|
|
368
368
|
|
|
369
369
|
Note: `:before_tool_call` can replace the `:call` hash to change what runs; `tool_call_started` (what the UIs show) and the rules see the final call.
|
|
370
370
|
|
|
371
|
+
### The call a hook sees
|
|
372
|
+
|
|
373
|
+
`event[:call]` is the same hash for every model format (Gemma, Qwen, the
|
|
374
|
+
chat path): `name:`, `content:`, `path:`, `scope:`, plus the tool's other
|
|
375
|
+
parameters by their own names (symbol keys). `content:` holds the tool's
|
|
376
|
+
main argument (`""` when it has none), `path:` the parameter named in its
|
|
377
|
+
column, and an argument the model left out is `nil`. Text values are
|
|
378
|
+
stripped of surrounding whitespace, except file text, an edit's
|
|
379
|
+
`old_text`/`new_text`, `env` and `options`.
|
|
380
|
+
|
|
381
|
+
| Tool | `content:` from | `path:` from | Own fields |
|
|
382
|
+
|------|-----------------|--------------|------------|
|
|
383
|
+
| `execute` | `command` | — | `cwd` |
|
|
384
|
+
| `read` | `path` | — | `start_line`, `end_line` |
|
|
385
|
+
| `write` | — | `path` | `content` |
|
|
386
|
+
| `edit` | — | `path` | `old_text`, `new_text`, `start_line`, `end_line` |
|
|
387
|
+
| `memory_read` | `name` | — | `scope` |
|
|
388
|
+
| `memory_write` | — | `name` | `content`, `scope`, `description`, `current_model_only` |
|
|
389
|
+
| `task_create` | `command` | — | `cwd`, `env` |
|
|
390
|
+
| `task_get` | `id` or `task_id` | — | — |
|
|
391
|
+
| `task_list` | — | — | — |
|
|
392
|
+
| `task_stop` | `id` or `task_id` | — | — |
|
|
393
|
+
| `task_wait` | `id` or `task_id` | — | `timeout`, `tail_lines`, `done_pattern` |
|
|
394
|
+
| `web_fetch` | `url` | — | — |
|
|
395
|
+
| `register_reminder` | `name` | — | `description`, `interval_minutes` |
|
|
396
|
+
| `cancel_reminder` | `name` | — | — |
|
|
397
|
+
| `list_reminders` | — | — | — |
|
|
398
|
+
| `list_sessions` | — | — | `cwd` |
|
|
399
|
+
| `send_note` | `text` | — | `session` |
|
|
400
|
+
| `delegate` | `task` | — | `model`, `session`, `wait`, `timeout` |
|
|
401
|
+
| `delegate_result` | — | — | `session`, `timeout` |
|
|
402
|
+
| `ask_user_question` | `question` | — | `question`, `options`, `header`, `multi_select`, `allow_freeform` |
|
|
403
|
+
|
|
404
|
+
A `write`'s `content:` is the file text; an `edit` carries `old_text:` and
|
|
405
|
+
`new_text:` (its `content:` is `""`). A plugin or MCP tool's call has its
|
|
406
|
+
arguments whole on `args:` (string keys) instead.
|
|
407
|
+
|
|
371
408
|
## Bundle Hooks (unified workflow bundle)
|
|
372
409
|
|
|
373
410
|
Bundles can ship executable guardrails alongside memories. A bundle with hooks lives as a directory with a `hooks/` subdirectory (flat, basename-keyed):
|
|
@@ -393,7 +430,7 @@ hooks:
|
|
|
393
430
|
guardrails.rb:
|
|
394
431
|
sha256: 1234...
|
|
395
432
|
event: before_tool_call
|
|
396
|
-
on_error: fail_closed # default
|
|
433
|
+
on_error: fail_closed # skip (default) | log | fail_closed (before_tool_call only)
|
|
397
434
|
priority: 10
|
|
398
435
|
audit.rb:
|
|
399
436
|
sha256: 5678...
|
|
@@ -410,12 +447,12 @@ Notes:
|
|
|
410
447
|
- `event` is required for auto-registration; a hook with no event is skipped.
|
|
411
448
|
- `sha256` is integrity (not authenticity). No signing in v1. Install records the sha256 of the copied file; at `Engine.new` a hook whose file differs is not loaded (reinstall the bundle after editing one by hand).
|
|
412
449
|
- Hook code is the bundle author's source of truth: on upgrade, hooks are overwritten; if the installed file was locally modified, a warning is emitted (`was locally modified; overwriting`).
|
|
413
|
-
-
|
|
450
|
+
- `on_error` defaults to `skip` for every event, `:before_tool_call` included: a raising hook is silent and the call goes ahead. `log` warns. `fail_closed` (only on `:before_tool_call`; on any other event it acts as `skip`) denies the call the hook raised on. Choose `fail_closed` for a hook that is a guardrail, whose failure should stop tool calls rather than let them through.
|
|
414
451
|
- A `fail_closed` `:before_tool_call` hook is required: if it is missing, fails to load or its sha256 differs, chi denies every tool call until it is fixed.
|
|
415
452
|
- A bundle can also ship YAML rules in `guardrails/*.yml`; see [Guardrails](guardrails.md#the-guardrails-bundle).
|
|
416
453
|
- Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
|
|
417
454
|
- Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
|
|
418
|
-
- A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md).
|
|
455
|
+
- A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md). There `ctx.notify`, `ctx.steer` and the other `ctx` helpers act as this page's `event[:notify]`… do for the event the block runs for.
|
|
419
456
|
- Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
|
|
420
457
|
- Installing a bundle executes its hook code at `Engine` startup. Only install bundles you trust, as you would a gem. Hooks are **not** executed at install time (copy-only); they are `module_eval`'d at `Engine.new` inside per-bundle `Samagotchi::Bundles::<name>` namespaces (no top-level `require` collisions). Keep hook files side-effect-free at load time; do work in `#call` — top-level side effects (require, IO, `at_exit`, global assignment) run once per `Engine.new` (class redefinition is idempotent).
|
|
421
458
|
|
|
@@ -19,5 +19,6 @@ Thought handling follows the Gemma guidance:
|
|
|
19
19
|
- Standard multi-turn: prior model thoughts are stripped from conversation history before the next turn.
|
|
20
20
|
- Function/tool-calling exception: during a single turn that includes tool calls, thoughts are not stripped between those tool-call rounds.
|
|
21
21
|
- Final model output returned to the caller is thought-stripped.
|
|
22
|
+
- While a generation streams, the thought channel goes to the stream's `thinking:` lane, not `text:` (`ThoughtStreamSplitter.for_profile`, fresh per generation), so the UIs show it as thinking and `:generation_progress` hooks count it in `thinking_chars`.
|
|
22
23
|
|
|
23
24
|
In short, raw thought blocks are treated as in-turn transient context, not durable history.
|
data/docs/plugins.md
CHANGED
|
@@ -304,18 +304,33 @@ end
|
|
|
304
304
|
### `chi.on(event, priority: 100) { |event, ctx| … }`
|
|
305
305
|
|
|
306
306
|
This is a bundle hook, the same as a `hooks/*.rb` file. See
|
|
307
|
-
[hooks.md](hooks.md#hook-events) for the events
|
|
308
|
-
`event[:ask_user]`, `event[:stop_turn]` and `event[:steer]` do; on `:after_turn`,
|
|
307
|
+
[hooks.md](hooks.md#hook-events) for the events; on `:after_turn`,
|
|
309
308
|
`event[:present]` sets how the answer is shown in the web
|
|
310
309
|
([Presenting the answer](hooks.md#presenting-the-answer-display-only)). Its label is
|
|
311
310
|
`plugin.rb (bundle my-bundle)`. The block may take only the event. If it
|
|
312
311
|
raises, the error is logged and the hook is skipped.
|
|
313
312
|
|
|
313
|
+
To act from the block, use `ctx`: `ctx.notify`, `ctx.ask_user`,
|
|
314
|
+
`ctx.steer`, `ctx.stop_turn`, `ctx.stop_generation`. Inside the block they
|
|
315
|
+
act for **this event** (the `event[:notify]`… helpers that plain hooks in
|
|
316
|
+
[hooks.md](hooks.md) get are the same thing, and a plugin doesn't need them):
|
|
317
|
+
|
|
318
|
+
- `ctx.stop_turn` in a `:before_tool_call` block also denies the call the
|
|
319
|
+
event is about.
|
|
320
|
+
- From `:after_turn` and `:session_end` there is no turn left:
|
|
321
|
+
`ctx.steer`, `ctx.stop_turn` and `ctx.stop_generation` do nothing and
|
|
322
|
+
return false.
|
|
323
|
+
- In a `:generation_progress` block `ctx.ask_user` asks no one (nil).
|
|
324
|
+
|
|
325
|
+
This holds on the block's own thread while it runs. A thread the block
|
|
326
|
+
starts doesn't inherit the event: there the helpers act as from a command,
|
|
327
|
+
on the session as it is when called ([The context](#the-context-ctx)), so
|
|
328
|
+
`ctx.steer` from it reaches whatever turn is running then.
|
|
329
|
+
|
|
314
330
|
#### Watching the stream
|
|
315
331
|
|
|
316
332
|
`chi.on(:generation_progress)` sees a response while it streams, in batches
|
|
317
|
-
(2000 chars or a second), and `
|
|
318
|
-
`ctx.stop_generation`) cuts it while the turn goes on: the model is asked
|
|
333
|
+
(2000 chars or a second), and `ctx.stop_generation` cuts it while the turn goes on: the model is asked
|
|
319
334
|
again. The rules (the thread it runs on, keeping it fast, what a cut does)
|
|
320
335
|
are in [hooks.md](hooks.md#watching-the-stream).
|
|
321
336
|
|
|
@@ -328,7 +343,7 @@ class Plugin
|
|
|
328
343
|
next if event[:thinking_chars] < LIMIT
|
|
329
344
|
|
|
330
345
|
# Once per generation: a cut generation fires no more.
|
|
331
|
-
if
|
|
346
|
+
if ctx.stop_generation("it thought for over #{LIMIT / 1000}k chars")
|
|
332
347
|
ctx.notify("thinking too long (#{event[:elapsed_ms] / 1000} s): cut", level: :warn)
|
|
333
348
|
end
|
|
334
349
|
end
|
|
@@ -449,7 +464,14 @@ name order, so the first bundle keeps the name.
|
|
|
449
464
|
## The context: `ctx`
|
|
450
465
|
|
|
451
466
|
Every handler gets the plugin's context. There is one per plugin for the
|
|
452
|
-
session's life, and each read gives the session as it is now.
|
|
467
|
+
session's life, and each read gives the session as it is now. It is a
|
|
468
|
+
plugin's one way to talk to the user and to the turn: the helpers below act
|
|
469
|
+
on the session now from a command, a tool, an init task or your own thread,
|
|
470
|
+
and for the event inside a `chi.on` block (see `chi.on` above).
|
|
471
|
+
|
|
472
|
+
Everything a plugin shows (notices, nudges, stop notices, questions) is
|
|
473
|
+
named by its bundle in every UI (`my-bundle> …`); the debug log keeps the
|
|
474
|
+
full label, `plugin.rb (bundle my-bundle)`.
|
|
453
475
|
|
|
454
476
|
| | |
|
|
455
477
|
|---|---|
|
|
@@ -461,13 +483,13 @@ session's life, and each read gives the session as it is now.
|
|
|
461
483
|
| `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
|
|
462
484
|
| `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 |
|
|
463
485
|
| `ctx.messages_partial?` | whether `ctx.messages` leaves out a running turn (the REPL mid-turn), so a plugin can say what its answer is about |
|
|
464
|
-
| `ctx.notify(text, level: :info)` | one line to the user,
|
|
486
|
+
| `ctx.notify(text, level: :info)` | one line to the user, labelled by the bundle (`my-bundle> …`). Every UI shows it, during a turn (a tool, a hook) or between turns (a command) |
|
|
465
487
|
| `ctx.card(title:, body: "", actions: [], level: :info, id: nil)` | a card in every UI, returning its id: see [Cards](#cards) |
|
|
466
|
-
| `ctx.ask_user(question:, options:, header: nil, allow_freeform: false)` | a question
|
|
488
|
+
| `ctx.ask_user(question:, options:, header: nil, allow_freeform: false)` | a single-select question through the question flow: `{selected:, freeform:, selected_indices:}`, or nil (no one to ask, cancelled, bad options) |
|
|
467
489
|
| `ctx.cancelled?` | whether the running turn was cancelled (a long tool should stop) |
|
|
468
|
-
| `ctx.steer(text)` | put text into the running turn
|
|
469
|
-
| `ctx.stop_turn(reason)` | stop the running turn after a warn notice with the reason
|
|
470
|
-
| `ctx.stop_generation(reason)` | cut the generation that is streaming
|
|
490
|
+
| `ctx.steer(text)` | put text into the running turn: its own user message at the loop's next boundary, shown as `my-bundle> nudged: …`. Returns true when queued, false with no turn running (it never starts one; that is `ctx.sessions`' send). Dropped (logged) if the model answers or the turn ends first. Safe from any thread: an anytime command, a hook (false from `:after_turn`), your own |
|
|
491
|
+
| `ctx.stop_turn(reason)` | stop the running turn after a warn notice with the reason; true when it stopped one now. In a `:before_tool_call` block it also denies that call |
|
|
492
|
+
| `ctx.stop_generation(reason)` | cut the generation that is streaming: the turn goes on and the model is asked again ([hooks.md](hooks.md#watching-the-stream)). Shows nothing: post your own notice. True when it cut one now |
|
|
471
493
|
| `ctx.ask_model(messages:, prompt:, …)` | a side answer from the session's model: see [Side answers](#side-answers-ctxask_model) |
|
|
472
494
|
| `ctx.sessions` | fork, send to and read other sessions: see [Other sessions](#other-sessions-ctxsessions) |
|
|
473
495
|
|
|
@@ -915,7 +937,7 @@ with a line and carry on unchanged.
|
|
|
915
937
|
|
|
916
938
|
`chi bundle install skills` (or the `dev` profile) installs the bundle shipped with chi
|
|
917
939
|
(`lib/samagotchi/bundles/skills/plugin.rb`): one anytime command, `chi.on`
|
|
918
|
-
hooks, `ctx.sessions.send`, `ctx.notify` and `
|
|
940
|
+
hooks, `ctx.sessions.send`, `ctx.notify` and `ctx.steer`. It has no memory
|
|
919
941
|
file; skills themselves work without it ([docs/memory.md](memory.md#skills)).
|
|
920
942
|
|
|
921
943
|
| | |
|
|
@@ -951,7 +973,8 @@ A plain read keeps no version.
|
|
|
951
973
|
skip it and go on without fixing the skill. In a turn that read a skill
|
|
952
974
|
(`memory_read` of a `skill_*` name, or `read` of its file), the first failing
|
|
953
975
|
tool call after it (an `execute` that exited non-zero, a tool error) steers
|
|
954
|
-
the model once
|
|
976
|
+
the model once (a call a guardrail or the user denied doesn't count as a
|
|
977
|
+
failed step): *"A step of skill release failed. Find out why before skipping
|
|
955
978
|
it; if the skill is out of date, fix it now: edit the step that changed in
|
|
956
979
|
its file (or memory_write the whole skill) and add a Changelog line."* No
|
|
957
980
|
steer once a skill read this turn has changed, by any tool. If the turn ends
|
data/docs/sessions.md
CHANGED
|
@@ -19,7 +19,7 @@ A session is deleted if **expired by age OR overflow by count** (unless `keep_st
|
|
|
19
19
|
|
|
20
20
|
**Attached by default:** plain `chi` and `chi --resume ID` run their session in a background worker and attach to it (`session.shared`, default `true`; env `SAMAGOTCHI_SESSION_SHARED`). The worker runs in the session's `working_directory`, so `!commands` and tools see the directory the session was started in. `session.shared: false` keeps the in-process REPL, `--no-shared` does for one run; a session the REPL has open can't be attached ("close it there first"). See [CLI: Sharing a session](cli.md#sharing-a-session).
|
|
21
21
|
|
|
22
|
-
**Stopping a worker:** `chi sessions stop ID` marks the session stopped, sends its worker TERM and waits (up to 10 s) until the worker has let go of `owner.lock`, so a `chi --resume ID` right after spawns a fresh worker. That is also how to restart a worker still running an older chi, which the attached terminal
|
|
22
|
+
**Stopping a worker:** `chi sessions stop ID` marks the session stopped (an empty `<id>/stopped` file, not a session.json field, which a worker saving at that moment would write over; a resume removes it), sends its worker TERM and waits (up to 10 s) until the worker has let go of `owner.lock`, so a `chi --resume ID` right after spawns a fresh worker. That is also how to restart a worker still running an older chi, which the attached terminal reports when a command or a question dismiss gets a 404, and the web when an answer, a dismiss, a command or a cancel does (`501 not_supported`; `BridgeClient.stale_worker_message`). The web's `POST /api/sessions/:id/stop` waits the same way, for 2 s.
|
|
23
23
|
|
|
24
24
|
**Deleting a session:** `chi sessions delete ID...`, `/exit --delete` in a terminal and the Web UI's delete all go through `SessionManager.delete_session`: it resolves a unique prefix, removes `<id>.json` and the whole `<id>/` dir, and returns what it removed. A session a plain REPL has open (`owner.lock` kind `tui`) is always refused. One a worker runs is refused unless the caller asks to stop it: then it stops the worker as `chi sessions stop` does and deletes once `owner.lock` is free (`--force` on the CLI, 10 s; the web always, 2 s; `/exit --delete` after the worker agreed to exit, 10 s). A worker that outlives the wait leaves the session in place. The web's route is `DELETE /api/sessions/:id` (200 `{status: "deleted", session_id, stopped}`; 409 `owned_by_tui` or `still_stopping`; 404).
|
|
25
25
|
|
|
@@ -6,6 +6,8 @@ require_relative "model_profile"
|
|
|
6
6
|
require_relative "bootstrap/probe"
|
|
7
7
|
require_relative "bootstrap/config_writer"
|
|
8
8
|
require_relative "bootstrap/bundles"
|
|
9
|
+
require_relative "cli/command"
|
|
10
|
+
require_relative "cli/flags"
|
|
9
11
|
|
|
10
12
|
module Samagotchi
|
|
11
13
|
# `chi bootstrap [TARGET]`: the first setup. It names a model server, works
|
|
@@ -15,6 +17,8 @@ module Samagotchi
|
|
|
15
17
|
# system bundle and the core profile, and on a terminal asks about dev.
|
|
16
18
|
# Prompts only on a terminal; a script gets the list and exit 2 instead.
|
|
17
19
|
class BootstrapCommand
|
|
20
|
+
include CLI::Command
|
|
21
|
+
|
|
18
22
|
USAGE = <<~TEXT
|
|
19
23
|
Usage: chi bootstrap [TARGET] [--name NAME] [--model ID] [--key-env VAR] [--no-test] [--dry-run]
|
|
20
24
|
Finds the model server at TARGET and writes config.yml for it.
|
|
@@ -37,6 +41,14 @@ module Samagotchi
|
|
|
37
41
|
check-in, guardrails), and on a terminal offers the dev bundles.
|
|
38
42
|
TEXT
|
|
39
43
|
|
|
44
|
+
FLAGS = CLI::Flags.new(help: CLI::Command::HELP_WORDS) do |f|
|
|
45
|
+
f.value "--name"
|
|
46
|
+
f.value "--model"
|
|
47
|
+
f.value "--key-env"
|
|
48
|
+
f.switch "--no-test"
|
|
49
|
+
f.switch "--dry-run"
|
|
50
|
+
end
|
|
51
|
+
|
|
40
52
|
PICK_SHOWN = 20
|
|
41
53
|
# A test request slower than this says it is waiting (a model loading).
|
|
42
54
|
LOADING_AFTER = 3
|
|
@@ -66,8 +78,8 @@ module Samagotchi
|
|
|
66
78
|
# @return [Integer] 0 written (or already there), 1 failed (a failed
|
|
67
79
|
# test still writes the config), 2 usage or a choice to make
|
|
68
80
|
def run
|
|
69
|
-
options = parse
|
|
70
|
-
return
|
|
81
|
+
options = parse
|
|
82
|
+
return options if options.is_a?(Integer)
|
|
71
83
|
|
|
72
84
|
key_env = options[:key_env]
|
|
73
85
|
return fail!("#{key_env} is not set; export it first (the API key; chi only writes its name)") if key_env && unset?(key_env)
|
|
@@ -85,26 +97,21 @@ module Samagotchi
|
|
|
85
97
|
|
|
86
98
|
private
|
|
87
99
|
|
|
100
|
+
def command_name = "chi bootstrap"
|
|
101
|
+
# A usage error's line ends with "(see chi bootstrap --help)".
|
|
102
|
+
def usage_on_error = nil
|
|
103
|
+
|
|
104
|
+
# @return [Hash, Integer] the options, or the exit status after the
|
|
105
|
+
# help or a usage error
|
|
88
106
|
def parse
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
arg = @argv.shift
|
|
92
|
-
case arg
|
|
93
|
-
when "-h", "--help", "help"
|
|
94
|
-
@stdout.puts(USAGE)
|
|
95
|
-
return { help: true }
|
|
96
|
-
when "--name", "--model", "--key-env"
|
|
97
|
-
options[arg.delete_prefix("--").tr("-", "_").to_sym] = @argv.shift or return usage_error("#{arg} needs a value")
|
|
98
|
-
when /\A--(name|model|key-env)=(.+)\z/ then options[Regexp.last_match(1).tr("-", "_").to_sym] = Regexp.last_match(2)
|
|
99
|
-
when "--no-test" then options[:no_test] = true
|
|
100
|
-
when "--dry-run" then options[:dry_run] = true
|
|
101
|
-
when /\A-/ then return usage_error("unknown option #{arg}")
|
|
102
|
-
else
|
|
103
|
-
return usage_error("one TARGET only (got #{options[:target]} and #{arg})") if options[:target]
|
|
107
|
+
parsed = parse_flags(FLAGS, @argv)
|
|
108
|
+
return parsed if parsed.is_a?(Integer)
|
|
104
109
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
options = parsed.options
|
|
111
|
+
target, extra = parsed.args
|
|
112
|
+
return usage_error("one TARGET only (got #{target} and #{extra})") if extra
|
|
113
|
+
|
|
114
|
+
options[:target] = target if target
|
|
108
115
|
if options[:key_env] && !options[:key_env].match?(ConfigFile::ENV_NAME_RE)
|
|
109
116
|
return usage_error("--key-env takes the variable's name (e.g. OPENROUTER_API_KEY), not the key")
|
|
110
117
|
end
|
|
@@ -119,8 +126,7 @@ module Samagotchi
|
|
|
119
126
|
candidates = begin
|
|
120
127
|
Bootstrap::Probe.candidates(options[:target])
|
|
121
128
|
rescue ArgumentError => e
|
|
122
|
-
usage_error(e.message)
|
|
123
|
-
@exit = 2
|
|
129
|
+
@exit = usage_error(e.message)
|
|
124
130
|
return nil
|
|
125
131
|
end
|
|
126
132
|
result = @probe.classify_target(candidates, key_env: key_env)
|
|
@@ -251,8 +257,7 @@ module Samagotchi
|
|
|
251
257
|
name = begin
|
|
252
258
|
writer.host_name(Bootstrap::ConfigWriter.derived_name(result.candidate.host), requested: options[:name], model_ids: ids)
|
|
253
259
|
rescue Bootstrap::ConfigWriter::Error => e
|
|
254
|
-
usage_error(e.message)
|
|
255
|
-
@exit = 2
|
|
260
|
+
@exit = usage_error(e.message)
|
|
256
261
|
return nil
|
|
257
262
|
end
|
|
258
263
|
outcome = writer.write(name: name, fields: fields, model: model, dry_run: options[:dry_run])
|
|
@@ -408,10 +413,5 @@ module Samagotchi
|
|
|
408
413
|
@exit = 1
|
|
409
414
|
nil
|
|
410
415
|
end
|
|
411
|
-
|
|
412
|
-
def usage_error(message)
|
|
413
|
-
@stderr.puts("chi bootstrap: #{message} (see chi bootstrap --help)")
|
|
414
|
-
nil
|
|
415
|
-
end
|
|
416
416
|
end
|
|
417
417
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../events"
|
|
4
|
+
|
|
3
5
|
module Samagotchi
|
|
4
6
|
class Bridge
|
|
5
7
|
# The last cards (Engine#show_card), hook notices (:hook_notice) and
|
|
@@ -25,9 +27,6 @@ module Samagotchi
|
|
|
25
27
|
# A failed turn leaves no prompt, so its cards stay before the next one.
|
|
26
28
|
class CardStore
|
|
27
29
|
CAPACITY = 20
|
|
28
|
-
# A failed turn's prompt goes back to the composer: no turn in the
|
|
29
|
-
# conversation.
|
|
30
|
-
TURN_ENDS = %i[turn_completed turn_canceled].freeze
|
|
31
30
|
|
|
32
31
|
def initialize(capacity: CAPACITY)
|
|
33
32
|
@capacity = capacity
|
|
@@ -79,7 +78,8 @@ module Samagotchi
|
|
|
79
78
|
when :turn_failed
|
|
80
79
|
@running = false
|
|
81
80
|
settle_during(after_turn: false)
|
|
82
|
-
|
|
81
|
+
# The turns that stay in the conversation.
|
|
82
|
+
when *Events::TURN_KEPT
|
|
83
83
|
@running = false
|
|
84
84
|
@turns_done += 1
|
|
85
85
|
settle_during(after_turn: true)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require_relative "../atomic_file"
|
|
6
|
+
require_relative "../events"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
class Bridge
|
|
@@ -18,7 +19,6 @@ module Samagotchi
|
|
|
18
19
|
# writes it, nor does a card between turns.
|
|
19
20
|
class PendingCard
|
|
20
21
|
FILE = "pending_card.json"
|
|
21
|
-
TURN_ENDS = %i[turn_completed turn_canceled turn_failed].freeze
|
|
22
22
|
|
|
23
23
|
# @param session_dir [String] the session's folder
|
|
24
24
|
# @return [Hash, nil] {id:, bundle:}, nil without a readable file
|
|
@@ -58,7 +58,7 @@ module Samagotchi
|
|
|
58
58
|
def fold(event)
|
|
59
59
|
case event[:type]
|
|
60
60
|
when :card then card(event)
|
|
61
|
-
when *
|
|
61
|
+
when *Events::TURN_END then remove
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|