samagotchi 0.80.0 → 0.82.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 +57 -1
- data/docs/architecture.md +9 -1
- data/docs/cli.md +9 -1
- data/docs/configuration.md +4 -0
- data/docs/guardrails.md +26 -3
- data/docs/plugins.md +9 -3
- data/docs/sessions.md +9 -1
- data/docs/sub-agent.md +19 -8
- data/docs/testing.md +19 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/bootstrap_command.rb +1 -1
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +4 -4
- data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +2 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/cli/parent_wait.rb +9 -3
- data/lib/samagotchi/engine.rb +31 -5
- data/lib/samagotchi/guardrails/data_readers.rb +214 -0
- data/lib/samagotchi/guardrails/rm_targets.rb +4 -2
- data/lib/samagotchi/guardrails/rules.rb +4 -2
- data/lib/samagotchi/guardrails/shell_lex.rb +166 -28
- data/lib/samagotchi/guardrails/shell_paths.rb +5 -3
- data/lib/samagotchi/guardrails/targets.rb +14 -0
- data/lib/samagotchi/host_config.rb +8 -8
- data/lib/samagotchi/idle_client.rb +36 -11
- data/lib/samagotchi/idle_recap.rb +6 -1
- data/lib/samagotchi/idle_target.rb +12 -4
- data/lib/samagotchi/llm/http.rb +7 -2
- data/lib/samagotchi/llm/usage.rb +9 -3
- data/lib/samagotchi/model_price.rb +16 -0
- data/lib/samagotchi/parent_report.rb +7 -3
- data/lib/samagotchi/plugin/context.rb +10 -3
- data/lib/samagotchi/plugin/service.rb +24 -1
- data/lib/samagotchi/recap_setup.rb +2 -1
- data/lib/samagotchi/reply_wait.rb +68 -2
- data/lib/samagotchi/send_command.rb +47 -8
- data/lib/samagotchi/session_metrics/side_totals.rb +117 -0
- data/lib/samagotchi/session_metrics.rb +49 -12
- data/lib/samagotchi/session_observer.rb +3 -0
- data/lib/samagotchi/side_usage.rb +38 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +3 -0
- data/lib/samagotchi/terminal_ui/model_segments_text.rb +27 -3
- data/lib/samagotchi/testing/boundary.rb +15 -0
- data/lib/samagotchi/testing/bundle.rb +35 -9
- data/lib/samagotchi/testing/host.rb +2 -2
- data/lib/samagotchi/testing/recorder.rb +7 -4
- data/lib/samagotchi/testing/sandbox.rb +5 -3
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +4 -1
- data/lib/samagotchi/web/public/app.js +3 -1
- data/lib/samagotchi/web/public/ctx.js +32 -6
- data/lib/samagotchi/web/public/info_bar_model.js +1 -1
- data/lib/samagotchi/web/public/lists_view.js +1 -1
- data/lib/samagotchi/web/public/question_cards.js +10 -5
- data/lib/samagotchi/web/public/stream_handlers.js +7 -0
- data/lib/samagotchi/web/public/timing.js +6 -1
- data/lib/samagotchi/web/session_summary.rb +2 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02be0edba759be799428a67ac174beba8a77c2869f3de8f677dec9a14977ae60
|
|
4
|
+
data.tar.gz: d28e0688aec5428264fc7a48162a7f4e03ea461371cc48aba4da3c64ecf2e9d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1df47ccd973923c62848fef02807f3982ede1b86ebf032425c1ecd311014103286803822a290b24f117d705e3e8f845b22cb4804e90cc731a8dd48a3946a3b6f
|
|
7
|
+
data.tar.gz: 3c901380929266d60c820ebba9ad3581bdb4392f498fbb43dcef6ba157e675c100a8dba752ee27a79d464692ffb0a407b75ead89961f38c89e6551bf8a9a6ada
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,60 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.82.0] - 2026-10-18
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `chi send --wait --format json` names the turn it waited for (`turn_id`), and `chi send --wait --turn T ID` waits
|
|
16
|
+
for exactly that turn: running, queued or the last one that ended. A parent waiting again after exit 3 or 4 uses it
|
|
17
|
+
(docs/sub-agent.md).
|
|
18
|
+
- Bundles: the experimental test kit's `before_tool_call_event` builds the real guardrail event without firing
|
|
19
|
+
bundle hooks.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `chi send --wait ID` with no message, on an idle session whose last turn already ended, returns that turn (its
|
|
24
|
+
reply, or its end when it left none) instead of timing out `running`.
|
|
25
|
+
- Web: a question or approval card arriving in the stage no longer snaps the stage to it; the stage eases it into
|
|
26
|
+
view.
|
|
27
|
+
- Guardrails: `rm` and protected-path checks treat a `$(cat <<'EOF' … EOF)` commit or PR message as data; a
|
|
28
|
+
substitution that runs, a piped message and a real command beside it are still checked.
|
|
29
|
+
- A late side request (an idle recap, `ctx.ask_model`) after the engine moved to another session no longer recreates
|
|
30
|
+
the deleted old session's folder.
|
|
31
|
+
- HTTP: failed-request messages in the log are one short UTF-8 line.
|
|
32
|
+
- Bundles, test kit: one `HookFailed` names every hook that failed; the boundary scan catches a `require_relative`
|
|
33
|
+
into chi's `lib/`; `before_tool_call_event` says when the guardrails didn't dispatch the event.
|
|
34
|
+
|
|
35
|
+
Update with `chi update` (the system bundle 0.82.0), then restart `chi web`.
|
|
36
|
+
|
|
37
|
+
## [0.81.0] - 2026-10-18
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- Stats: side requests (the idle recap, `/btw` and plugins' `ctx.ask_model`) are counted on their own
|
|
42
|
+
`side requests:` line in `/stats` and the web's ctx tooltip, with their tokens and cost; the session's totals stay
|
|
43
|
+
its turns'.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- Guardrails: a `command:` rule no longer matches the text of a heredoc its command only reads as data
|
|
48
|
+
(`git commit -F -`, `gh pr create --body-file -`, `cat` to stdout, a `"$(cat <<'EOF' …)"` commit message); a
|
|
49
|
+
heredoc fed to a shell or an interpreter, or piped on (also from a `( … )` or `{ … }` group), is still
|
|
50
|
+
matched. The bundle's `chi-answer-piped` (guardrails bundle 0.9.1) no longer asks about stdin from
|
|
51
|
+
`/dev/null`.
|
|
52
|
+
- Guardrails: the `rm` and `touches` checks now see a heredoc that bash runs after all — piped on from a `( … )` or
|
|
53
|
+
`{ … }` group, fed through `<( … )` / `>( … )`, or written with `&>` / `>& file` and then run — which they used to
|
|
54
|
+
read as plain data.
|
|
55
|
+
- Plugins: a `chi.service` that starts lazily from a tool or hook handler runs its start block outside the handler,
|
|
56
|
+
so Stop no longer cancels the service's startup `ctx.http` calls (as docs/plugins.md says).
|
|
57
|
+
- Bundles: the test kit's sandbox also points `XDG_DATA_HOME` and `XDG_CACHE_HOME` at its temp folder, so a bundle
|
|
58
|
+
test no longer writes to your own data or cache home when you set them.
|
|
59
|
+
- `chi bootstrap`: re-running it against a server the config already has says so, without the misleading "saved as …"
|
|
60
|
+
line before it.
|
|
61
|
+
|
|
62
|
+
Update with `chi update` (the system bundle 0.81.0, `guardrails` 0.9.1, `slow-commands` 0.2.3), then restart
|
|
63
|
+
`chi web`.
|
|
64
|
+
|
|
11
65
|
## [0.80.0] - 2026-10-17
|
|
12
66
|
|
|
13
67
|
### Added
|
|
@@ -3310,7 +3364,9 @@ and long-lived sessions.
|
|
|
3310
3364
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3311
3365
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3312
3366
|
|
|
3313
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3367
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.82.0...HEAD
|
|
3368
|
+
[0.82.0]: https://github.com/dm1try/samagotchi/compare/v0.81.0...v0.82.0
|
|
3369
|
+
[0.81.0]: https://github.com/dm1try/samagotchi/compare/v0.80.0...v0.81.0
|
|
3314
3370
|
[0.80.0]: https://github.com/dm1try/samagotchi/compare/v0.79.0...v0.80.0
|
|
3315
3371
|
[0.79.0]: https://github.com/dm1try/samagotchi/compare/v0.78.0...v0.79.0
|
|
3316
3372
|
[0.78.0]: https://github.com/dm1try/samagotchi/compare/v0.77.0...v0.78.0
|
data/docs/architecture.md
CHANGED
|
@@ -338,6 +338,14 @@ carries the generation's `speed` (`{decode_tps, source}`, null without one), its
|
|
|
338
338
|
counts (`prompt_tokens`, `cached_tokens`, `cache_write_tokens`: null when the server reported no writes) and
|
|
339
339
|
those running `tokens` and `model_segments`: the Engine closes the generation in `SessionMetrics#finish_generation` before relaying
|
|
340
340
|
it, so the web's info bar updates per generation without parsing chunk payloads.
|
|
341
|
+
Side requests (the idle recap, a plugin's `ctx.ask_model` such as `/btw`) are counted apart, in a `side` block
|
|
342
|
+
(only when there were any; `SessionMetrics::SideTotals`: requests, token sums, `cost_sum` null until reported,
|
|
343
|
+
`cost_estimate_sum` from the target host's price, `by_kind`): `IdleClient` hands each reply's usage to its
|
|
344
|
+
`on_usage:` callback, `Engine#note_side_usage`, which folds it (`SessionMetrics#add_side`) and announces
|
|
345
|
+
`:side_usage` (`kind`, the whole `side` block) under the event lock, so the blocks arrive in fold order, then saves
|
|
346
|
+
`analytics.json` between turns (a running turn's end saves it). `tokens`, `model_segments` and the turn records
|
|
347
|
+
stay the turns' only. `SessionMetrics#persist` is serialized (its own mutex, taken before the metrics one), so two
|
|
348
|
+
threads' saves land in snapshot order.
|
|
341
349
|
|
|
342
350
|
The page's frequent reads of `GET /api/sessions/:id` stay light too:
|
|
343
351
|
- `?tail=1&recent=1` (each turn's end, a cancel, an answer display): `{tail, session: {id, status,
|
|
@@ -523,7 +531,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
523
531
|
| Attached context | `context_sources.rb` (`ContextSources`), `context_poller.rb`, `context_absorber.rb`, `peer_sources.rb` (`PeerSources`: the sources peer bundles declare) | External sources a session keeps fresh, and the peer loop's wakes ([context.md](context.md)). |
|
|
524
532
|
| LLM context strategies | `llm_context_strategy.rb`, `llm_context_view.rb`, `llm_context_stale.rb`, `llm_context_forget.rb` | What a request sends of the history (none, stale, forget) ([internals/llm-context-forget.md](internals/llm-context-forget.md)). |
|
|
525
533
|
| Session chains | `session_continue.rb` (`SessionContinue`), `session_chain.rb` (`SessionChain`) | A session that continues an earlier one. `SessionChain.opener_for` gives the text web `continue →` prefills in the new link: the previous link's own first typed message when that link continues one itself, else nil (the 201's `opener`; not saved) ([sessions.md](sessions.md)). |
|
|
526
|
-
| Analytics | `analytics_file.rb` (`AnalyticsFile`), `session_metrics/totals.rb` (`SessionMetrics::Totals`), `session_metrics/model_segment.rb` (`SessionMetrics::ModelSegment`) | A session's `analytics.json` (`SessionMetrics#persist`); the sums over its turn records (`Totals`: tokens, cost, speeds, kept as running counters) and their split per model after a `/model` switch (`ModelSegment`: a run of turns on one model, the `model_segments` rows). |
|
|
534
|
+
| Analytics | `analytics_file.rb` (`AnalyticsFile`), `session_metrics/totals.rb` (`SessionMetrics::Totals`), `session_metrics/model_segment.rb` (`SessionMetrics::ModelSegment`) | A session's `analytics.json` (`SessionMetrics#persist`); the sums over its turn records (`Totals`: tokens, cost, speeds, kept as running counters) and their split per model after a `/model` switch (`ModelSegment`: a run of turns on one model, the `model_segments` rows). `session_metrics/side_totals.rb` (`SessionMetrics::SideTotals`) and `side_usage.rb` (`SideUsage`): the side requests' sums, apart from the turns'. |
|
|
527
535
|
| Images / vision | `image_store.rb` (`ImageStore`), `vision_support.rb`, `vision_context.rb` | Stored prompt images and whether a model takes them. |
|
|
528
536
|
| Toolkits | `toolkit.rb` (`Toolkit`), `toolkit_binding.rb` (`ToolkitBinding`, the Engine's seam), `toolkit_command.rb` (`/toolkit`), `tools/registry.rb` (`Registry#toolkit`, `#left_out_names`), `web/public/toolkit_chip.js` | Which registered tools a session's prompts declare: saved in the session, resolved at a turn's start (`Engine#apply_toolkit!`), named in the prompt's not-loaded line, a kit's prompt text after the appended files; `chi.toolkit` and `ctx.use_toolkit` for plugins ([configuration.md](configuration.md#toolkits)). |
|
|
529
537
|
| Tool index | `tools/tool_index.rb` (`Tools::ToolIndex`) | `find_tools` and `call_tool`: the tools a session knows but doesn't declare one by one (an mcp bundle's servers through `chi.tool_source`, the built-ins its toolkit leaves out), searched and called by name ([plugins.md](plugins.md)). |
|
data/docs/cli.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
- `chi sessions list|stop|restart|archive|unarchive|delete|prune|clean|stats` — manage persisted sessions; `list` shows this git project's, `list --scope=all` every one, a delegated session with `↳ <parent>`, `list --archived` the archived ones too (see [Sessions](sessions.md)). `stats ID` prints a session's cost, tokens and progress (a live worker's `GET stats`, else its saved `analytics.json`; `--format json` one object) without running a model turn. A usage error (an unknown subcommand or flag, a flag missing its value, `stop`, `restart` or `delete` with no ids, a bad `list --format` or `--scope`, a `--days`, `--keep` or `--limit` that isn't a whole number) exits 2; an unknown or refused session exits 1
|
|
19
19
|
- `chi note [--source NAME] [-m TEXT] (ID|PREFIX)... | --all` — add a context note (TEXT or stdin) to sessions: background the model sees on its next turn; it starts no turn (see [Sessions: Context notes](sessions.md#context-notes))
|
|
20
20
|
- `chi context <add|push|ls|show|refresh|rm|mute|unmute> …` — attach live external text to sessions (a command chi runs every so often, or text pushed in); the model gets a short note when it changes and reads it with `context_read`; `chi context add <URL>` attaches a GitHub PR with the github-pr bundle (see [Attached context](context.md))
|
|
21
|
-
- `chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it; `--queue` runs it as a turn of its own after the running one, and `--queue --wait` waits for that turn's own answer); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending; `--format json`
|
|
21
|
+
- `chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it; `--queue` runs it as a turn of its own after the running one, and `--queue --wait` waits for that turn's own answer); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending — to an idle session whose last turn already ended, that ended turn's reply or end comes back; `--wait --turn T ID` with no message waits for exactly turn T, whether running, queued or last ended; `--format json` includes its `turn_id` when known, for waiting again after exit 3 or 4; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
|
|
22
22
|
- `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). An approval can be denied, not allowed, unless `guardrails.parent_approvals: once` (a convention, not a security boundary; see [Guardrails](guardrails.md#approvals-from-a-parent-agent))
|
|
23
23
|
- `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text or images to a session (live, stopped or new) as a message (⏎, a turn runs) or a context note (⌘⏎) (see [Desktop helper](desktop.md))
|
|
24
24
|
- `chi models [--format text|json] [--timeout S] [TEXT]` — list the models every configured host offers, as the names `--model` takes (see [Listing the models](#listing-the-models))
|
|
@@ -486,6 +486,14 @@ Generations under 8 tokens or 100 ms get no speed of their own, nor does a
|
|
|
486
486
|
reply without server token counts or a non-streamed one. A delegated child
|
|
487
487
|
keeps its own numbers: a parent's tokens and cost leave them out.
|
|
488
488
|
|
|
489
|
+
Side requests have their own line, right after `cost:` (in its place without one), only when the session made any:
|
|
490
|
+
`side requests: 3 (recap 2, btw 1) · 14.2k in / 380 out · $0.0012 + ~$0.0009 (not in the totals above)`. It
|
|
491
|
+
counts the idle recap (`recap`), `/btw` and every other plugin's `ctx.ask_model` (under the asking bundle's name),
|
|
492
|
+
with their tokens and cost: a reported cost as is, else estimated from the price of the model they asked, on its
|
|
493
|
+
host (as a turn's, `~`). They are never in the tokens, `models:` or `cost:` lines above it, which stay the turns'.
|
|
494
|
+
A recap thrown away (stale, overdue) still counts: it was billed. `chi sessions stats` shows the line too (saved
|
|
495
|
+
between turns), and the web's ctx tooltip has the same words.
|
|
496
|
+
|
|
489
497
|
After a `/model` switch (or a hook's) the tokens line becomes a `models:` block, a row per run of turns on one
|
|
490
498
|
model, in order (A → B → A is three rows): the model (`host:model` as asked), when (`10:43–now` while it runs; with
|
|
491
499
|
the day when the rows span several), turns, tokens in/out, its average speed and cost. Under the row of the model
|
data/docs/configuration.md
CHANGED
|
@@ -573,6 +573,10 @@ hosts:
|
|
|
573
573
|
- The estimate is saved apart from the reported cost (`cost_estimate` per turn, `cost_estimate_sum` in the
|
|
574
574
|
session's totals in `analytics.json`) and shows with a `~`.
|
|
575
575
|
- A server that sends no cached counts has its whole prompt priced at `input`: an overestimate.
|
|
576
|
+
- Side requests (the idle recap, `/btw`, a plugin's `ctx.ask_model`) are priced by the same rule from the price of
|
|
577
|
+
the model they asked on its host: the recap's host and model (`recap.*`, else the session's), `ask_model(model:)`'s
|
|
578
|
+
host. A recap using only `recap.base_url` has no host price. They count on `/stats`' own `side requests:` line,
|
|
579
|
+
never in the session's `cost:`.
|
|
576
580
|
|
|
577
581
|
## Thinking
|
|
578
582
|
|
data/docs/guardrails.md
CHANGED
|
@@ -230,7 +230,7 @@ guardrails:
|
|
|
230
230
|
rules:
|
|
231
231
|
- id: git-push
|
|
232
232
|
tool: shell # execute + task_create; or a tool name, a glob, or a list
|
|
233
|
-
command: '\bgit\s+push\b' # Ruby regex on the command
|
|
233
|
+
command: '\bgit\s+push\b' # Ruby regex on the command (data heredoc bodies left out)
|
|
234
234
|
verdict: ask # ask | deny
|
|
235
235
|
reason: git push publishes commits
|
|
236
236
|
scopes: [once, session, repo] # optional; default all four
|
|
@@ -248,6 +248,23 @@ guardrails:
|
|
|
248
248
|
|
|
249
249
|
A rule gives at least one of `tool`, `command`, `path`, `git`, `touches`, `rm` and `memory`.
|
|
250
250
|
|
|
251
|
+
`command:` reads the command without the body of a heredoc its command
|
|
252
|
+
only reads as data: `cat` or `tee` to stdout, `git commit`/`git tag` with
|
|
253
|
+
`-F -`/`--file -`, `gh pr|issue`/`gh release` with `--body-file -`/
|
|
254
|
+
`--notes-file -`, and a `"$(cat <<'EOF' … EOF)"` given to `-m`, `--body`
|
|
255
|
+
or another message option. Each counts only when its command ends at a
|
|
256
|
+
newline, `;`, `&&` or `||` and sits outside any `( … )` or `{ … }` group;
|
|
257
|
+
the `<<TAG` word and the terminator line stay in the text, so a rule on
|
|
258
|
+
`<<` or on what follows the terminator still matches. A heredoc fed to
|
|
259
|
+
anything else (`bash`, an interpreter, `ssh`, a wrapper), an unquoted one
|
|
260
|
+
whose body holds `$(…)` or backticks, a piped one, a file-writing
|
|
261
|
+
`cat`/`tee` (a `>&2` or `>&-` writes no file; `>& file` does), a
|
|
262
|
+
here-string and an unterminated heredoc all stay in it. An fd opened
|
|
263
|
+
earlier and written to by number (`exec 3>r.sh; cat <<E >&3`) isn't
|
|
264
|
+
followed, so that body counts as data: out of scope here, which is
|
|
265
|
+
chi-nah's job for a model that goes looking. Approvals, the approval
|
|
266
|
+
question and hooks see the whole command.
|
|
267
|
+
|
|
251
268
|
`memory: remove` matches a `memory_write` with `remove: true` (it moves a
|
|
252
269
|
memory into the bundle trash). Nothing else asks about it: a memory's file is
|
|
253
270
|
never outside the repo (below), so without such a rule any session, a delegate
|
|
@@ -493,7 +510,7 @@ only by the user: these change a model's system prompt for later sessions (see
|
|
|
493
510
|
[Model notes](memory.md#model-notes); a shell command reaches them only under
|
|
494
511
|
`shell-touches-chi`'s text heuristic); and answering chi's
|
|
495
512
|
questions around `chi answer`: `chi --attach` or `chi -p` with stdin from a
|
|
496
|
-
pipe, a here-string or a file (`chi-answer-piped`), and `curl`/`wget` to a
|
|
513
|
+
pipe, a here-string or a file, not `/dev/null` (`chi-answer-piped`), and `curl`/`wget` to a
|
|
497
514
|
session's `/answer` route (`chi-answer-http`), both once or for the session.
|
|
498
515
|
`chi answer` itself isn't asked about. It denies writes into `.git/hooks`.
|
|
499
516
|
|
|
@@ -591,7 +608,13 @@ else goes to chi's own rules. It needs nah installed; see its README for the set
|
|
|
591
608
|
|
|
592
609
|
Text matching on shell commands stops accidents, not a model set on getting
|
|
593
610
|
around it: `sh -c`, base64, a script written earlier, `git -C` variants and
|
|
594
|
-
aliases can get past a regex
|
|
611
|
+
aliases can get past a regex, and these rules are built for trusted models (a
|
|
612
|
+
model set on getting around them is chi-nah's job). A heredoc counts as data
|
|
613
|
+
only for the consumers listed under [Rules in config.yml](#rules-in-configyml)
|
|
614
|
+
(`cat`/`tee` to stdout, `git commit`/`git tag` `-F -`, `gh`'s body and notes
|
|
615
|
+
files, a `"$(cat <<'EOF' … EOF)"` message): anything else keeps its body in
|
|
616
|
+
the text a rule sees, and a function or alias shadowing a reader in the same
|
|
617
|
+
command isn't seen at all. `outside_repo` covers the file tools and git
|
|
595
618
|
only: `sed -i`, `cp` or `cat > file` into another folder from the shell aren't
|
|
596
619
|
asked about, and neither is git run through `sh -c`, a script, an alias, `make`
|
|
597
620
|
or a folder in a variable. `!cmd` lines typed by you are not checked. The only
|
data/docs/plugins.md
CHANGED
|
@@ -858,6 +858,10 @@ next turn never see it, and no hook fires. It returns the answer text.
|
|
|
858
858
|
It goes through the host's OpenAI API (`/v1/chat/completions`), which
|
|
859
859
|
llama.cpp serves on a native host too.
|
|
860
860
|
|
|
861
|
+
Each call counts in the session's side requests, under the bundle's name: `/stats`' `side requests:` line and the
|
|
862
|
+
web's ctx tooltip (`2 (btw 1, recap 1) · …`), with its tokens and its cost (reported, else estimated from the target
|
|
863
|
+
model's price), never in the session's own totals.
|
|
864
|
+
|
|
861
865
|
## Calling services: `ctx.http`
|
|
862
866
|
|
|
863
867
|
```ruby
|
|
@@ -923,7 +927,8 @@ Every call also takes `cancel:`, a `Samagotchi::CancellationController`, which r
|
|
|
923
927
|
- **Stop.** A call made for the turn (a tool, a hook's handler) stops when the turn is stopped, and a call inside an
|
|
924
928
|
init task when chi shuts down; a call from your own thread, a `chi.service` or an anytime command has only its
|
|
925
929
|
timeout unless you pass `cancel:`. After the turn has ended (an `:after_turn` or `:session_end` hook of a
|
|
926
|
-
stopped turn) a call is not cancelled by that stop.
|
|
930
|
+
stopped turn) a call is not cancelled by that stop. A service's start block counts as the service's even when its
|
|
931
|
+
first `value` comes from a handler.
|
|
927
932
|
- **The log.** Each call writes one `http` line to the debug log: INFO (ERROR when it fails) with `bundle=`,
|
|
928
933
|
`purpose=plugin`, the method, the URL **without its query**, the status and the time. Never headers or bodies.
|
|
929
934
|
A plugin that polls a dead service therefore logs an ERROR per poll.
|
|
@@ -1129,6 +1134,7 @@ require "samagotchi/testing"
|
|
|
1129
1134
|
bundle = Samagotchi::Testing::Bundle.load("path/to/my-bundle", settings: { "every" => 3 },
|
|
1130
1135
|
host: { session_id: "s1", cwd: Dir.pwd })
|
|
1131
1136
|
bundle.fire(:after_turn, status: "completed") # → the event, after the hooks
|
|
1137
|
+
event = bundle.before_tool_call_event({ name: "read", path: "a.txt" }, params: "read a.txt")
|
|
1132
1138
|
bundle.command("/mine stop") # → what the command returned
|
|
1133
1139
|
bundle.tool("my_tool", path: "a.txt") # → the text the model reads
|
|
1134
1140
|
verdict = bundle.tool_call({ name: "execute", content: "rm -rf /" })
|
|
@@ -1164,8 +1170,8 @@ bundle.shutdown
|
|
|
1164
1170
|
`bundle.set(cancelled: true)` makes a call without `cancel:` raise `HttpCancelled`.
|
|
1165
1171
|
- **A hook that raises fails the call** (`Testing::HookFailed`, with the hook's error) where chi would log it and
|
|
1166
1172
|
go on; `bundle.lenient { … }` collects them in `bundle.errors` instead.
|
|
1167
|
-
- **Sandbox.** A test runs with temp config and
|
|
1168
|
-
log), `HOME`, `CLAUDE_CONFIG_DIR` and `CODEX_HOME`, no git config of yours, no Keychain and none of your
|
|
1173
|
+
- **Sandbox.** A test runs with temp config, state, data and cache homes (config.yml, memories, `ctx.data_dir`, the
|
|
1174
|
+
debug log), `HOME`, `CLAUDE_CONFIG_DIR` and `CODEX_HOME`, no git config of yours, no Keychain and none of your
|
|
1169
1175
|
`SAMAGOTCHI_*` settings; `sandbox.config(hash)` writes its config.yml.
|
|
1170
1176
|
- **RSpec:** `require "samagotchi/testing/rspec"`; example groups tagged `:bundle` (a spec under a bundle's own
|
|
1171
1177
|
`spec/` is tagged by itself) get `load_bundle(dir)`, `shipped_bundle(name)` and `sandbox`, each example in its
|
data/docs/sessions.md
CHANGED
|
@@ -51,6 +51,14 @@ chi sessions stats ID [--format text|json] # cost, tokens and progres
|
|
|
51
51
|
|
|
52
52
|
`chi sessions stats ID` prints what `/stats` prints in a terminal, headed by `<id8> <status> <model>`: a live worker answers its Bridge's `GET stats` (no model call, and the command never starts a worker); without one, or when the worker does not answer in time, the same snapshot is rebuilt from the session's saved `analytics.json` (and the line says so when a worker was live). `--format json` prints one object `{session_id, status, live, metrics}` with the snapshot as `metrics` (its `turn_records` / `tool_records` are the recent ones only: a live worker's newest finished turn and any not yet saved, the disk snapshot's newest saved turn; `analytics.json` holds them all); a session with no `analytics.json` yet reports `metrics: null` (`(no metrics yet)` in text) and still exits 0. An unknown or ambiguous id exits 1 with the same wording `chi sessions stop` uses.
|
|
53
53
|
|
|
54
|
+
`analytics.json` also has a `side` block when the session made side requests (the idle recap, `/btw` and other
|
|
55
|
+
plugins' `ctx.ask_model`): `requests`, `prompt_sum`, `completion_sum`, `cached_sum`, `cache_write_sum`,
|
|
56
|
+
`reasoning_sum`, `cost_sum` (the reported cost; null until one was), `cost_estimate_sum` (from the target host's
|
|
57
|
+
price; 0 without one) and `by_kind` (`{recap: {requests, cost_sum, cost_estimate_sum}, btw: {…}}`, in first-seen
|
|
58
|
+
order: `recap`, else the asking bundle's name). It has no records: the block itself is saved (at once between turns,
|
|
59
|
+
by the turn's end during one) and loaded; it is never in `tokens` or `model_segments`. `chi sessions stats --format
|
|
60
|
+
json` has it in `metrics.side`; a file without it (older, or no side requests) has none.
|
|
61
|
+
|
|
54
62
|
Each turn record in `analytics.json` names the model the turn ran on as `model_ref` (the asked `host:model`, from its first generation; `model` stays the name the server says it served); a turn stopped before it asked a model has none. The first turn after a `/model` switch (or a hook's) to another model also records what its first generation prefilled on the new model: `switch_prefill_tokens` (the prompt less the cached reads; none under 128 tokens, or for a generation with estimated counts) and `switch_prefill_cost_estimate` (from the new model's configured price, no completion; none without a price). A session saved before this has no `model_ref` on its records.
|
|
55
63
|
|
|
56
64
|
A session whose worker waits for an answer (a question, a hook's question, a guardrail approval, or the step-limit question of a turn that ran out of steps, which waits between turns) reads `waiting` in the status column, and json has `waiting: "question"|"approval"|"hook"|"continue"` and `waiting_id`, the question's id for `chi answer --question` (both null otherwise). A delegate's approval its parent relayed to its own card (see *Delegating*) reads `waiting (in parent ab12)`, json `relayed_to: "ab12…"` (the parent's short id, else null). Only while a worker runs it: a question a dead worker left in the file shows the saved status. Answer it with `chi answer`, the web or `chi --attach ID`; `chi sessions stop ID` frees the worker instead.
|
|
@@ -200,7 +208,7 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
|
|
|
200
208
|
- `--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).
|
|
201
209
|
- 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.
|
|
202
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.
|
|
203
|
-
- `--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
|
|
211
|
+
- `--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).
|
|
204
212
|
- `--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 and toolkit, so `--dir`, `--llm-context*`, `--thinking`, `--toolkit`, `--tool` and `--no-tool` 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: …`.
|
|
205
213
|
- 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.
|
|
206
214
|
- 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.
|
data/docs/sub-agent.md
CHANGED
|
@@ -30,6 +30,7 @@ sources](context.md#peer-sources-the-peer-loop).
|
|
|
30
30
|
```sh
|
|
31
31
|
chi send --new --wait --format json [--dir D] [--model M] [--timeout S] -m "task"
|
|
32
32
|
chi send --wait --format json [--timeout S] [-m "follow-up"] ID
|
|
33
|
+
chi send --wait --format json [--timeout S] --turn T ID
|
|
33
34
|
chi send --queue --wait --format json [--timeout S] -m "follow-up" ID
|
|
34
35
|
chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]
|
|
35
36
|
chi answer ID --question QID --dismiss [--timeout S] [--format json]
|
|
@@ -60,6 +61,11 @@ Exit codes, both commands:
|
|
|
60
61
|
|
|
61
62
|
With `--format json` stdout is one JSON object (see
|
|
62
63
|
[Starting a session](sessions.md#starting-a-session) for every field):
|
|
64
|
+
when the wait is about a known turn, it includes `turn_id`. Use
|
|
65
|
+
`chi send --wait --format json --turn <turn_id> <session_id>` to wait for
|
|
66
|
+
that exact turn again after exit 4, or after exit 3 if someone else will
|
|
67
|
+
answer the question. A turn id can name the running turn, a queued turn or
|
|
68
|
+
the last turn that ended; any other id is an error.
|
|
63
69
|
|
|
64
70
|
```json
|
|
65
71
|
{"status":"question","session_id":"297da360-…","question":{"id":"3f1c…","kind":"question",
|
|
@@ -115,7 +121,7 @@ a parent may deny it (`--option Deny --text "why"`, `--text` alone, or
|
|
|
115
121
|
and tell your user`. For an approval, the question block and `answer_with` give
|
|
116
122
|
the deny command, not `--option N` or `chi --attach`. The block also offers to
|
|
117
123
|
leave it open: tell your user it waits in chi web (the bell and badge show
|
|
118
|
-
it there), and `chi send --wait --format json ID`
|
|
124
|
+
it there), and `chi send --wait --format json --turn <turn_id> ID` waits until
|
|
119
125
|
they answer it. With
|
|
120
126
|
`guardrails.parent_approvals: once` in chi's config.yml (it has no environment
|
|
121
127
|
variable), "Allow once" goes through; the wider scopes never do,
|
|
@@ -161,7 +167,11 @@ answers from any local process.
|
|
|
161
167
|
- `chi send --wait ID` with no message waits past a question already reported
|
|
162
168
|
(the step-limit question too), until someone answers it. It waits for the
|
|
163
169
|
turn running when it starts (or the one the step-limit question continues),
|
|
164
|
-
and prints that turn's reply, not a queued one's after it.
|
|
170
|
+
and prints that turn's reply, not a queued one's after it. To an idle
|
|
171
|
+
session whose last turn already ended (no question standing), that ended
|
|
172
|
+
turn is the answer: its reply comes back (`answered`), or its end when it
|
|
173
|
+
left no reply (`failed`, `canceled`, `limit`, …) — so a turn that finished
|
|
174
|
+
in the gap between waits is reported, not waited for.
|
|
165
175
|
- An answer to a question that is no longer open (you answered it in the web
|
|
166
176
|
first) isn't sent; `chi answer` says so and waits for what comes next. Its
|
|
167
177
|
JSON then has `"answered_here": false` (absent when the answer went in), so
|
|
@@ -185,18 +195,19 @@ Start a task: `chi send --new --wait --format json --timeout 500 -m "<task>"`
|
|
|
185
195
|
user, showing them the question and its options; if you can't ask (no user in
|
|
186
196
|
this run), stop and report the question and options instead of guessing. To
|
|
187
197
|
answer, run the `answer_with` command with `--option N` (1-based, repeat for
|
|
188
|
-
multi-select) and/or `--text "…"`. It returns the same JSON. If you
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
multi-select) and/or `--text "…"`. It returns the same JSON. If you leave it
|
|
199
|
+
open for someone else to answer, wait again with `--turn <turn_id>` from the
|
|
200
|
+
JSON. If you won't answer, add `--dismiss` instead: chi does nothing it asked
|
|
201
|
+
about, finishes with what it found, and the reply comes back (exit 0).
|
|
191
202
|
- `question.kind: approval`: chi wants to run something its user's guardrails
|
|
192
203
|
flagged, and allowing it is up to your user. Deny it (the `answer_with`
|
|
193
204
|
command: `--option Deny --text "why"`), and tell your user what it wanted to
|
|
194
205
|
run and why. Or leave it open: tell your user it waits in chi web (the
|
|
195
|
-
session's short id), then `chi send --wait --format json --timeout 500 <session_id>`
|
|
206
|
+
session's short id), then `chi send --wait --format json --timeout 500 --turn <turn_id> <session_id>`
|
|
196
207
|
waits until they answer it there. Never allow it yourself, through
|
|
197
208
|
`chi answer`, a piped `chi --attach` or chi's web API.
|
|
198
|
-
- `status: running` (exit 4): still working. Wait again:
|
|
199
|
-
`chi send --wait --format json --timeout 500 <session_id>`.
|
|
209
|
+
- `status: running` (exit 4): still working. Wait for that same turn again:
|
|
210
|
+
`chi send --wait --format json --timeout 500 --turn <turn_id> <session_id>`.
|
|
200
211
|
- `question.kind: continue`: chi ran out of steps before it answered. If it is
|
|
201
212
|
getting somewhere, continue it (`answer_with`, `--option Continue`, with
|
|
202
213
|
`--text "…"` to steer it on the way); if not,
|
data/docs/testing.md
CHANGED
|
@@ -83,6 +83,25 @@ The kit's own specs, in `spec/samagotchi/testing/`, keep it and the bundles hone
|
|
|
83
83
|
bundle's `Boundary::EXCEPTIONS` entry, each with why. A new internal reference fails it, and so does an exception
|
|
84
84
|
no longer used. Fix the reference (the kit or a stable API), or add the exception with its why.
|
|
85
85
|
|
|
86
|
+
## Guardrail cases corpus
|
|
87
|
+
|
|
88
|
+
`spec/fixtures/guardrails/cases.jsonl` holds one labelled tool call per line, run by
|
|
89
|
+
`spec/guardrails/cases_corpus_spec.rb` against a fixed rule set (`spec/fixtures/guardrails/rules.yml`) or the
|
|
90
|
+
shipped guardrails bundle's rules. A case:
|
|
91
|
+
|
|
92
|
+
- `id` (required, unique, kebab-case), `tool` (required: `execute`, `task_create`, `write`, `edit` or `read`),
|
|
93
|
+
`command` (a shell tool) or `path` (a file tool), `cwd` (optional: the call's `cwd:`, relative to the spec's
|
|
94
|
+
temp repo), `expect` (required), `rule` (a rule id), `rules` (optional: `fixture`, the default, or
|
|
95
|
+
`guardrails-bundle`), `source` (required: where the case came from: `heredoc-fp`, `heredoc-bypass`,
|
|
96
|
+
`chi-answer-piped`) and `note` (optional: why the label is what it is).
|
|
97
|
+
- `expect` is one of two kinds: `match` / `no_match` say whether rule `rule` matches the call (`Rule#matches?`),
|
|
98
|
+
and `allow` / `ask` / `deny` the whole rule set's decision (`Rules#check`, not the Gate's core checks); with
|
|
99
|
+
`rule` given, an ask or deny must also be decided by that rule.
|
|
100
|
+
|
|
101
|
+
Add a case with every guardrail false positive or miss you fix, under the `source` it came from. Never relabel
|
|
102
|
+
a case to make a spec pass: a case that goes red means the code is wrong. The spec also keeps the corpus
|
|
103
|
+
well formed (unique ids, known keys and labels, a rule that exists in its set).
|
|
104
|
+
|
|
86
105
|
## While you work: affected specs
|
|
87
106
|
|
|
88
107
|
```sh
|
|
@@ -280,7 +280,7 @@ module Samagotchi
|
|
|
280
280
|
# neither url: nor host: is Anthropic's own API (HostConfig's rule).
|
|
281
281
|
def location(fields)
|
|
282
282
|
url = fields["url"].to_s.strip
|
|
283
|
-
url = ANTHROPIC_URL if url.empty? && anthropic?(fields) && fields["host"].to_s.strip.empty? && fields["port"].to_s.strip.empty?
|
|
283
|
+
url = HostConfig::ANTHROPIC_URL if url.empty? && anthropic?(fields) && fields["host"].to_s.strip.empty? && fields["port"].to_s.strip.empty?
|
|
284
284
|
unless url.empty?
|
|
285
285
|
uri = URI.parse(url)
|
|
286
286
|
path = uri.path.to_s.chomp("/")
|
|
@@ -337,8 +337,8 @@ module Samagotchi
|
|
|
337
337
|
@exit = usage_error(e.message)
|
|
338
338
|
return nil
|
|
339
339
|
end
|
|
340
|
-
explain_name(base, name, reason)
|
|
341
340
|
outcome = writer.write(name: name, fields: fields, model: model, dry_run: options[:dry_run])
|
|
341
|
+
explain_name(base, name, reason) unless outcome.kind == :exists
|
|
342
342
|
code = report(outcome, model)
|
|
343
343
|
return code unless INSTALLS_BUNDLES.include?(outcome.kind)
|
|
344
344
|
|
|
@@ -176,12 +176,12 @@ rules:
|
|
|
176
176
|
# ── answering chi's own questions around chi answer ───────────────────
|
|
177
177
|
# A parent agent answers a child's approval with chi answer, which holds it
|
|
178
178
|
# to guardrails.parent_approvals. These shapes go around that: chi --attach
|
|
179
|
-
# or chi -p with stdin from a pipe, a here-string or a file
|
|
180
|
-
# those as a parent too, but an older
|
|
181
|
-
# Bridge's or the web's answer route.
|
|
179
|
+
# or chi -p with stdin from a pipe, a here-string or a file other than
|
|
180
|
+
# /dev/null (a chi after 0.12 treats those as a parent too, but an older
|
|
181
|
+
# child doesn't), and a POST to the Bridge's or the web's answer route.
|
|
182
182
|
- id: chi-answer-piped
|
|
183
183
|
tool: shell
|
|
184
|
-
command: '(\|[^;&|\n]*\bchi\s([^;&|\n]*\s)?(--attach|--prompt|-p)\b|\bchi\s([^;&|\n]*\s)?(--attach|--prompt|-p)\b[^;&|\n]*<)'
|
|
184
|
+
command: '(\|[^;&|\n]*\bchi\s([^;&|\n]*\s)?(--attach|--prompt|-p)\b|\bchi\s([^;&|\n]*\s)?(--attach|--prompt|-p)\b[^;&|\n]*<(?!\s*/dev/null(?![\w./-])))'
|
|
185
185
|
verdict: ask
|
|
186
186
|
scopes: [once, session]
|
|
187
187
|
reason: answers chi's questions from a pipe or a file, around chi answer
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: guardrails
|
|
3
|
-
version: 0.9.
|
|
3
|
+
version: 0.9.1
|
|
4
4
|
scope: system
|
|
5
5
|
description: Optional default guardrail rules (git push/rewrites, rm -rf wide, curl | sh, git hooks, chi's own config and attached context from the shell, chi context add --cmd, chi broadcast, answering chi around chi answer, removing a memory, writing a memory that reaches the system prompt (identity, model notes) or a model overlay; in strict mode also git rebase and writes and git in another checkout outside the session's repo), asks only small models get (git checkout --/restore, git stash drop), and a note for the model
|
|
6
6
|
trust_level: reviewed
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: slow-commands
|
|
3
|
-
version: 0.2.
|
|
3
|
+
version: 0.2.3
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Remembers commands that ran past the execute limit in a project; the next identical execute is denied once a turn with advice to run it with task_create and task_wait; /slow-commands lists and forgets them"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
plugin:
|
|
8
8
|
file: plugin.rb
|
|
9
|
-
sha256: sha256:
|
|
9
|
+
sha256: sha256:249615f1d8529ab020ea9872bb42b00463594f20a9ec44fecc4e0f58d34dce91
|
|
10
10
|
# Execute::HANDED_OVER, read when the plugin loads (0.59.0); after_tool_call's
|
|
11
11
|
# call:, elapsed_ms: and result: (0.57.0)
|
|
12
12
|
requires_chi: ">= 0.59.0"
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
# `env X=… cmd` is `X=… cmd`, an `env` with -C/-i/-u (or any other option)
|
|
22
22
|
# stays as typed. The entry's `shown` is the last run as typed. Input that ends inside a quote, $(…)
|
|
23
23
|
# or heredoc is never recorded, nor one with a $(…), backticks or a heredoc
|
|
24
|
-
#
|
|
24
|
+
# a data reader reads as data (ShellLex: cat, tee, git commit -F -, gh …
|
|
25
|
+
# --body-file -): ShellLex makes those one placeholder, so different
|
|
25
26
|
# commands would share a key.
|
|
26
27
|
#
|
|
27
28
|
# after_tool_call records: timed out (killed at the limit), handed over as
|
|
@@ -62,19 +62,25 @@ module Samagotchi
|
|
|
62
62
|
# a --wait with no message is about (ReplyWait.own_turn)
|
|
63
63
|
# @param queued [Boolean] +turn+ is a queued message's (ReplyWait's
|
|
64
64
|
# queued:)
|
|
65
|
+
# @param ended_before [Boolean] a last_turn that ended before the
|
|
66
|
+
# baseline is the answer (ReplyWait's ended_before:) — the
|
|
67
|
+
# --wait-with-no-message's settled session
|
|
68
|
+
# @param report_turn_id [String, nil] the turn id to include in the
|
|
69
|
+
# report, even when ReplyWait uses the next-reply path
|
|
65
70
|
# @return [Integer] the exit status
|
|
66
71
|
def wait_for_reply(id, cursor:, baseline:, timeout:, owner_grace: self.class::WORKER_GONE_AFTER, turn: nil,
|
|
67
|
-
queued: false)
|
|
72
|
+
queued: false, ended_before: false, report_turn_id: turn)
|
|
68
73
|
result = ReplyWait.call(id, state_dir: @state_dir, cursor: cursor, timeout: timeout, baseline: baseline,
|
|
69
74
|
owner_grace: owner_grace, poll_interval: self.class::POLL_INTERVAL, turn: turn,
|
|
70
|
-
queued: queued)
|
|
75
|
+
queued: queued, ended_before: ended_before)
|
|
76
|
+
result.turn_id = report_turn_id if report_turn_id && !report_turn_id.to_s.empty?
|
|
71
77
|
result.text = result.text&.dup&.force_encoding(Encoding::UTF_8)&.scrub
|
|
72
78
|
@reported = true
|
|
73
79
|
ParentReport.report(result, session_id: id, stdout: @stdout, stderr: @stderr, command: command_name,
|
|
74
80
|
json: @json, timeout: timeout, extra: report_extra)
|
|
75
81
|
rescue Interrupt
|
|
76
82
|
if @json
|
|
77
|
-
@stdout.puts(ParentReport.json_line(ReplyWait::Result.new(status: :canceled), session_id: id,
|
|
83
|
+
@stdout.puts(ParentReport.json_line(ReplyWait::Result.new(status: :canceled, turn_id: report_turn_id), session_id: id,
|
|
78
84
|
extra: report_extra))
|
|
79
85
|
@stdout.flush
|
|
80
86
|
@reported = true
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -552,6 +552,30 @@ module Samagotchi
|
|
|
552
552
|
@session_observer.notify(type: :recap_ready, recap: recap, generation: generation, covered: covered)
|
|
553
553
|
end
|
|
554
554
|
|
|
555
|
+
# Count one side request (a recap, a plugin's ctx.ask_model: IdleClient's
|
|
556
|
+
# on_usage) in the session's side line, apart from its turns' totals.
|
|
557
|
+
# Folded and announced as :side_usage under the event lock, so the side
|
|
558
|
+
# blocks reach the UIs in fold order (the lock order event lock →
|
|
559
|
+
# metrics mutex is the existing one); then, between turns, saved to
|
|
560
|
+
# analytics.json (a running turn's end or checkpoint saves it), unless
|
|
561
|
+
# the session is gone (no session file: a late recap thread must not
|
|
562
|
+
# recreate a deleted session's folder; the folder itself may not exist
|
|
563
|
+
# yet before a first turn). Safe from any thread: the recap's, a
|
|
564
|
+
# plugin's, the turn's.
|
|
565
|
+
# @param usage [SideUsage]
|
|
566
|
+
# @return [void]
|
|
567
|
+
def note_side_usage(usage)
|
|
568
|
+
synchronize_events do
|
|
569
|
+
block = @metrics.add_side(usage)
|
|
570
|
+
@session_observer.notify(type: :side_usage, kind: usage.kind, side: block)
|
|
571
|
+
end
|
|
572
|
+
sid = @metrics.session_id
|
|
573
|
+
return if sid.nil? || turn_running?
|
|
574
|
+
return unless File.exist?(Session.session_file(sid, state_dir: session_state_dir))
|
|
575
|
+
|
|
576
|
+
@metrics.persist(state_dir: session_state_dir)
|
|
577
|
+
end
|
|
578
|
+
|
|
555
579
|
# @return [SessionMetrics] the per-session analytics collector
|
|
556
580
|
attr_reader :metrics
|
|
557
581
|
|
|
@@ -2544,9 +2568,9 @@ module Samagotchi
|
|
|
2544
2568
|
declared_tools: -> { toolkit_explained.declared.map(&:name) },
|
|
2545
2569
|
use_toolkit: ->(name) { use_toolkit(name) },
|
|
2546
2570
|
cancel_controller: ->(handler: false) { plugin_cancel_controller(handler: handler) },
|
|
2547
|
-
ask_model: lambda { |request, timeout:, max_tokens:, cancel_controller:, model: nil|
|
|
2571
|
+
ask_model: lambda { |request, timeout:, max_tokens:, cancel_controller:, model: nil, source: nil|
|
|
2548
2572
|
ask_side_model(request, timeout: timeout, max_tokens: max_tokens, cancel_controller: cancel_controller,
|
|
2549
|
-
model: model)
|
|
2573
|
+
model: model, source: source)
|
|
2550
2574
|
},
|
|
2551
2575
|
model_name: -> { effective_model_ref },
|
|
2552
2576
|
model_key: -> { @model_key },
|
|
@@ -2591,13 +2615,15 @@ module Samagotchi
|
|
|
2591
2615
|
# IdleClient, so it shares nothing with the turn's backend. The client
|
|
2592
2616
|
# is kept while the target and timeout stay (what it learned, e.g. a
|
|
2593
2617
|
# refused thinking-off, holds for the next request); the session's id
|
|
2594
|
-
# goes with the request.
|
|
2618
|
+
# goes with the request. Its usage counts in the session's side line
|
|
2619
|
+
# (#note_side_usage) under +source+, the asking bundle's name.
|
|
2595
2620
|
# @return [String] the answer
|
|
2596
|
-
def ask_side_model(request, timeout:, max_tokens:, cancel_controller:, model: nil)
|
|
2621
|
+
def ask_side_model(request, timeout:, max_tokens:, cancel_controller:, model: nil, source: nil)
|
|
2597
2622
|
target = model.nil? ? session_model_recap_target : configured_side_target(model)
|
|
2598
2623
|
client = side_model_client(target, timeout)
|
|
2599
2624
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
2600
|
-
answer = client.ask(request, max_tokens: max_tokens, cancel_controller: cancel_controller, session_id: @session&.id
|
|
2625
|
+
answer = client.ask(request, max_tokens: max_tokens, cancel_controller: cancel_controller, session_id: @session&.id,
|
|
2626
|
+
kind: source.to_s.empty? ? "ask" : source.to_s, on_usage: method(:note_side_usage))
|
|
2601
2627
|
Log.info(:plugins, "ask_model", model: target.label, answer_model: answer.model, chars: answer.text.length,
|
|
2602
2628
|
ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round)
|
|
2603
2629
|
answer.text
|