samagotchi 0.120.0 → 0.121.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 +35 -1
- data/docs/configuration.md +29 -10
- data/docs/internals/participants.md +3 -1
- data/docs/internals/prompt-caching.md +7 -6
- data/docs/plugins.md +24 -8
- data/docs/testing.md +3 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +1 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
- data/lib/samagotchi/bundles/mcp/plugin.rb +5 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +1 -0
- data/lib/samagotchi/kernel_loop.rb +5 -0
- data/lib/samagotchi/plugin/api.rb +6 -3
- data/lib/samagotchi/terminal_ui/question_slot.rb +19 -8
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +37 -1
- data/lib/samagotchi/toolkit.rb +50 -26
- data/lib/samagotchi/toolkit_binding.rb +32 -0
- data/lib/samagotchi/toolkit_command.rb +23 -0
- data/lib/samagotchi/tools/deferred_tools.rb +10 -2
- data/lib/samagotchi/tools/registry.rb +20 -6
- data/lib/samagotchi/tools/tool_index.rb +103 -20
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/participant_review.rb +9 -5
- data/lib/samagotchi/web/public/forms.css +12 -0
- data/lib/samagotchi/web/public/index.html +7 -5
- data/lib/samagotchi/web/public/level_chip.js +2 -2
- data/lib/samagotchi/web/public/llm_context_chip.js +4 -4
- data/lib/samagotchi/web/public/participant.html +1 -0
- data/lib/samagotchi/web/public/participant.js +13 -4
- data/lib/samagotchi/web/public/participants_panel.js +3 -3
- data/lib/samagotchi/web/public/question_card.js +15 -1
- data/lib/samagotchi/web/public/question_cards.js +11 -2
- data/lib/samagotchi/web/public/review_history.js +10 -2
- data/lib/samagotchi/web/public/toolkit_chip.js +3 -0
- data/lib/samagotchi/web/tunnel.rb +10 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78051f4f253f400d0b791fe2694f0dc3d07a19e7d1268ef30557ff3a780d8d64
|
|
4
|
+
data.tar.gz: c0bf5fbba8c530df5bf56a3947addddc0a9952a39f2077ddd5f59129683ba8f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c438bac307263d1a56cd9a97b56a2c3ab7175d9be0944c4e327fb92a57c9fdd9ffb82bdac8ec7048ac780a4a9e5f3545a3186dfcb3fec7842e687e2d47099307
|
|
7
|
+
data.tar.gz: 2738c2cc9f4f81f34c3c88f02568066dced57578a88ac6d1e301ca62508a3e343179a97a7d6ef9d6778f463a00648d2aa0f0b1aded820e8bce7442b2601a0d8a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ and commands may change between minor versions. How releases are made:
|
|
|
7
7
|
[docs/releasing.md](docs/releasing.md).
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.121.0] - 2026-10-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Toolkit kits pick the MCP servers their sessions reach: `groups: [chrome]` in `toolkit.kits` (names or globs; no
|
|
15
|
+
key = the base's; the built-ins reach every server, `review` none). `/toolkit` switches them from the next turn
|
|
16
|
+
(asking first: the cached prompt changes). A call into a server the kit doesn't reach is refused without starting
|
|
17
|
+
it, naming a kit that reaches it; its deferred stub says why, `/mcp` marks such servers, and the web's toolkit chip
|
|
18
|
+
lists a restricted kit's servers. Kits without `groups:` send the same request as before.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `ask_user_question`'s description asks the model, when it recommends one option, to put it first and end its
|
|
23
|
+
label with " (Recommended)" (eval 0005: Flash Next and DeepSeek v4.1-flash mark it 5/5 on a decision question,
|
|
24
|
+
0/5 before; an answer comes back with the suffix, so `chi answer --option` takes that exact label or the number).
|
|
25
|
+
- The recommended option shows as such: chi web's question card marks its row in the accent colour with a small
|
|
26
|
+
"recommended" tag (the label shown, and the answered line, without the suffix; nothing is pre-checked), and the
|
|
27
|
+
terminal's question paints its row bold. Approval and step-limit cards are unchanged.
|
|
28
|
+
- chi web's select boxes have one shape in every dialog: the Invite panel's person/agent and Expires now match the
|
|
29
|
+
text fields beside them (flat box, same height and corners, one thin caret), as the Thinking and LLM context
|
|
30
|
+
dialogs' selects already did, in place of the browser's own pill.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- claude-peer (0.4.4): `pair --inbox-socket PATH` says the remote socket is forwarded to this Claude's socket, not
|
|
35
|
+
from it (`ssh -R`).
|
|
36
|
+
- An ended participant link forgets its comment history even when its first review read is refused.
|
|
37
|
+
- The participant review status stays idle when a live snapshot has no current turn and its status is stale.
|
|
38
|
+
- The participant usage row no longer says "0 queued" when no worker is running.
|
|
39
|
+
- The participant usage row counts link time down and shows its end while the page stays open.
|
|
40
|
+
|
|
41
|
+
Update with `chi update` (the mcp bundle 0.7.2, claude-peer 0.4.4, the dev profile 0.7.3).
|
|
42
|
+
|
|
10
43
|
## [0.120.0] - 2026-10-26
|
|
11
44
|
|
|
12
45
|
### Added
|
|
@@ -4518,7 +4551,8 @@ and long-lived sessions.
|
|
|
4518
4551
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
4519
4552
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
4520
4553
|
|
|
4521
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
4554
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.121.0...HEAD
|
|
4555
|
+
[0.121.0]: https://github.com/dm1try/samagotchi/compare/v0.120.0...v0.121.0
|
|
4522
4556
|
[0.120.0]: https://github.com/dm1try/samagotchi/compare/v0.119.0...v0.120.0
|
|
4523
4557
|
[0.119.0]: https://github.com/dm1try/samagotchi/compare/v0.118.0...v0.119.0
|
|
4524
4558
|
[0.118.0]: https://github.com/dm1try/samagotchi/compare/v0.117.0...v0.118.0
|
data/docs/configuration.md
CHANGED
|
@@ -1400,6 +1400,8 @@ toolkit:
|
|
|
1400
1400
|
kits: # your own, on a base
|
|
1401
1401
|
audit: { base: lean, add: [web_fetch, delegate], remove: [memory_write] }
|
|
1402
1402
|
terse: { base: lean, prompt: "- Be concise in your responses" }
|
|
1403
|
+
web: { base: lean, groups: [chrome] }
|
|
1404
|
+
coding: { base: lean, groups: [] }
|
|
1403
1405
|
```
|
|
1404
1406
|
|
|
1405
1407
|
| toolkit | declares |
|
|
@@ -1412,8 +1414,21 @@ toolkit:
|
|
|
1412
1414
|
| `review` | `read`, `memory_read` (not `forget_outputs`, even under the `forget` layer): what a [participant's](internals/participants.md#turns) turn may call, whatever the session declares (the review boundary, [guardrails](guardrails.md#a-participants-review-boundary)). Reserved: `toolkit.kits.review` is refused ("review is reserved for participants' turns"), and a bundle can't declare it. A session may run on it too, as any kit |
|
|
1413
1415
|
|
|
1414
1416
|
- **Plugin tools** are declared in every toolkit unless its `remove:` names them, and so is `forget_outputs` under
|
|
1415
|
-
the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:` and `
|
|
1416
|
-
key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name
|
|
1417
|
+
the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:`, `prompt:` and `groups:`
|
|
1418
|
+
(any other key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name
|
|
1419
|
+
(`review`'s least of all: it is the participants' boundary). A bundle declares its own with `chi.toolkit`
|
|
1420
|
+
([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil-groups-nil)).
|
|
1421
|
+
- **Tool-index groups.** `groups:` selects source groups by name or glob (`chrome`, `db*`); for the MCP bundle
|
|
1422
|
+
each group is a server. Selectors are sorted and deduplicated and match the groups declared when plugins load.
|
|
1423
|
+
An omitted key inherits the base's selectors; `full` and `lean` (and `child`, which is on `lean`) reach all
|
|
1424
|
+
groups, `review` (the participants' kit) reaches none, `groups: ["*"]` matches every group, and `groups: []`
|
|
1425
|
+
reaches none. A kit whose base doesn't resolve (an unknown name, a cycle) falls back to `full`, which reaches
|
|
1426
|
+
every group.
|
|
1427
|
+
A selector that matches no group in that load-time snapshot warns once. `find_tools` and `call_tool` are
|
|
1428
|
+
declared only when the toolkit reaches a source group or has a reachable built-in. Calls naming an unreached
|
|
1429
|
+
group are refused before guardrails and do not start its server. The first-run init wait is unchanged: it can
|
|
1430
|
+
still block the first turn even when its server's group is unreached; `start: eager` and stale-cache refreshes
|
|
1431
|
+
also still start. `/mcp` marks servers outside the current toolkit.
|
|
1417
1432
|
- **A kit's prompt text.** `prompt:` (text, several lines allowed) goes into the system prompt of the sessions on
|
|
1418
1433
|
that kit, verbatim, with no header: after AGENTS.md and the appended files, before the memory indexes. A kit
|
|
1419
1434
|
without one inherits its base's (`audit: {base: terse, add: [web_fetch]}` stays concise); `prompt: ""` clears
|
|
@@ -1422,12 +1437,13 @@ toolkit:
|
|
|
1422
1437
|
fits the kit's work (the coordinator bundle's `coding`: be concise): one that isn't text or is over 2000
|
|
1423
1438
|
characters is left out with a warning, and long text belongs in `append_system_prompt:`. `/toolkit` shows it
|
|
1424
1439
|
(`prompt: …`), `/stats` its kit and size (`kit prompt:`), the web's tools chip in its tooltip.
|
|
1425
|
-
- **Saved at creation,
|
|
1440
|
+
- **Saved at creation, definitions resolve at worker start.** A session stores its toolkit as named
|
|
1426
1441
|
(`"toolkit": {"name": "lean", "add": [], "remove": []}`) when it is created: `toolkit.default`, `toolkit.child`
|
|
1427
1442
|
for a delegate child (or the kit its `delegate` call names), or `chi --toolkit NAME` / `--tool` / `--no-tool` ([cli](cli.md)). Changing `toolkit.default`
|
|
1428
1443
|
doesn't move existing sessions, not even when their worker is woken again; a session from before toolkits is on
|
|
1429
|
-
`full`. A continue and a fork keep the toolkit. `/toolkit` shows and changes it. The
|
|
1430
|
-
|
|
1444
|
+
`full`. A continue and a fork keep the toolkit name. `/toolkit` shows and changes it. The kit's tools, prompt
|
|
1445
|
+
text and group selectors are part of its definition, not the session file: edits reach a saved name at its next
|
|
1446
|
+
worker start.
|
|
1431
1447
|
- **Left out: reachable or removed.** A built-in missing from a kit's base list (`lean` has no `delegate`; a
|
|
1432
1448
|
`kits:` entry on `base: lean` or `child` inherits that) is left out softly: it is **reachable**. `find_tools` searches it as group `chi` (`chi/delegate`, with its inputSchema) beside
|
|
1433
1449
|
the MCP servers' tools, and `call_tool(tool: "chi/delegate", args: {…})` runs it as `delegate` itself, through the
|
|
@@ -1435,10 +1451,10 @@ toolkit:
|
|
|
1435
1451
|
tool in a toolkit's `remove:`, a `--no-tool` or a `/toolkit remove` is **removed**: hard, also after an add, and
|
|
1436
1452
|
call_tool can't reach it (`child` removes `memory_write`, `delegate` and `delegate_result`, so a child on it
|
|
1437
1453
|
starts no children). To keep a built-in away from the model, name it under `remove:`. `find_tools` and `call_tool`
|
|
1438
|
-
are declared when a toolkit has a reachable built-in
|
|
1454
|
+
are declared when a toolkit has a reachable built-in or reaches a source group; `full` with no reachable source
|
|
1439
1455
|
declares neither, and removing them there (`--no-tool call_tool`) leaves nothing out: no prompt line, no chip.
|
|
1440
1456
|
- **On `api: anthropic`, reachable tools are callable by name.** Every tool `call_tool` reaches (the reachable
|
|
1441
|
-
built-ins,
|
|
1457
|
+
built-ins, tools from reached MCP groups as `mcp_<server>_<tool>`) also goes in the request's tools deferred
|
|
1442
1458
|
(`defer_loading`: Messages keeps them out of the model's view until something loads them, at a flat ~100 tokens).
|
|
1443
1459
|
A direct call of one (`list_sessions`, `mcp_fs_read`) runs, under the same guardrails, approvals and rows as
|
|
1444
1460
|
through `call_tool` (an MCP tool's call is `call_tool`'s). A `find_tools` hit **loads** its matches (up to 5):
|
|
@@ -1455,10 +1471,13 @@ toolkit:
|
|
|
1455
1471
|
the route: `Error: delegate isn't declared in this session (toolkit lean); call it with call_tool(tool:
|
|
1456
1472
|
"chi/delegate", args: {…}) (find_tools gives its inputSchema).` A removed tool's: `Error: memory_write isn't
|
|
1457
1473
|
loaded in this session (toolkit child) and can't be loaded from here; only the user can (/toolkit add
|
|
1458
|
-
memory_write).`
|
|
1474
|
+
memory_write).` An MCP tool in an unreached group gets a toolkit-specific refusal before guardrails; a deferred
|
|
1475
|
+
stub for it points to the toolkit instead of suggesting a search. `/toolkit` lists group selectors and the
|
|
1476
|
+
change; the web's toolkit chip says its source-group scope in the tooltip.
|
|
1459
1477
|
- **Applied at a turn's start.** A change (`/toolkit`, a plugin's `ctx.use_toolkit`) is saved at once and the next
|
|
1460
|
-
turn declares it, with its kit's prompt text; once the session has a model reply, `/toolkit`
|
|
1461
|
-
("Changes that restart the prompt cache"), also
|
|
1478
|
+
turn declares it, with its kit's prompt text and group filter; once the session has a model reply, `/toolkit`
|
|
1479
|
+
asks first ("Changes that restart the prompt cache"), also when only the group selectors change and the
|
|
1480
|
+
declared tools stay the same.
|
|
1462
1481
|
- **A name that doesn't resolve** (a typo, a bundle that was uninstalled) declares every tool for that process,
|
|
1463
1482
|
with one `toolkit>` warning at the first turn; the saved name stays, so the bundle's return brings it back.
|
|
1464
1483
|
- **Versions.** A toolkit's definition (its tools and its prompt text) can change with chi or a bundle, which
|
|
@@ -94,7 +94,9 @@ participant** and **one role**, until it expires or is revoked.
|
|
|
94
94
|
block under the lock, and `App#code_identity_ok?` declines unless the trade is proxied with the grant's identity.
|
|
95
95
|
Declined, the code stays and the trade answers `token_bound`, so a forwarded link opened by someone else still
|
|
96
96
|
opens, once, for the account it was sent to. Owner codes and unbound grants' codes are used up whatever the gate
|
|
97
|
-
says, as before.
|
|
97
|
+
says, as before. Owner codes burn on purpose (user decision 2026-10-26, don't "align" them with bound grants): an
|
|
98
|
+
owner code is the whole control plane, so one presented by another identity has leaked and is killed at once;
|
|
99
|
+
the owner prints a new link (spec: proxy_gate_spec "uses up an owner's code someone else presents").
|
|
98
100
|
- **Which identity string.** The grant binds to whatever the header this run trusts carries: through Okta and
|
|
99
101
|
oauth2-proxy the work email (`alex@corp.com`), through Tailscale Serve the Tailscale login (`alex@github`, or a
|
|
100
102
|
private-relay address). The same person is two strings: bind to the one that proxy sends. A request from a
|
|
@@ -79,11 +79,13 @@ right-hand column happens.
|
|
|
79
79
|
`Toolkit::Resolved` the Registry holds, so a switch that changes only the text is a toolkit change
|
|
80
80
|
(`apply_toolkit!` → `reset!`). `full` and kits without one send the bytes they sent before.
|
|
81
81
|
- find_tools' description is a function of the toolkit and the source groups **at load**: `ToolIndex#declare`
|
|
82
|
-
snapshots
|
|
83
|
-
`ToolkitBinding#apply!`)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
snapshots source group names and description lines (name, summary, callable count). `ToolIndex#refresh!` (from
|
|
83
|
+
`ToolkitBinding#apply!`) filters that snapshot by the new toolkit and rebuilds the `chi` line from the built-ins
|
|
84
|
+
it reaches; it never consults live group names. The same group filter drives searches, deferred MCP tools and
|
|
85
|
+
whether `find_tools`/`call_tool` are declared (`Registry#index_needed?`). An MCP server that finishes booting
|
|
86
|
+
between load and the first turn changes search answers only, so two sessions on the same toolkit with the same
|
|
87
|
+
MCP cache send the same bytes. Kits that omit `groups:` keep their previous request bytes; switching only
|
|
88
|
+
groups changes the prompt key and asks `/toolkit` to confirm the cache cost once the session has history.
|
|
87
89
|
- A worker's `execute` and `task_create` descriptions carry the finish wake's sentence ("if you end your turn before
|
|
88
90
|
it finishes, chi tells you when it does"; `Engine.new(task_finish_notes:)`, set once as the worker builds its
|
|
89
91
|
Engine); the `--no-shared` REPL's, and a worker's under `session.task_wakes: off`, don't. A session that moves
|
|
@@ -240,4 +242,3 @@ is prefilled once. The native and the chat loop use the same plan.
|
|
|
240
242
|
`cache_prompt: true` on every native request.
|
|
241
243
|
- **OpenRouter / Anthropic:** the default cache TTL is 5 minutes (`cache.ttl: 1h` for an hour, see above).
|
|
242
244
|
Provider routing (which upstream serves a request) also decides whether a cache is there to hit.
|
|
243
|
-
|
data/docs/plugins.md
CHANGED
|
@@ -579,13 +579,16 @@ end
|
|
|
579
579
|
- Each task runs once per session start. A `-p … --non-interactive` run
|
|
580
580
|
starts them with its turn and shows nothing but the answer.
|
|
581
581
|
|
|
582
|
-
### `chi.toolkit(name, base:, add: [], remove: [], prompt: nil)`
|
|
582
|
+
### `chi.toolkit(name, base:, add: [], remove: [], prompt: nil, groups: nil)`
|
|
583
583
|
|
|
584
584
|
A toolkit sessions can be put on: `base`'s tools (`full`, `lean`, `child`, a
|
|
585
585
|
config or bundle toolkit) with `add` declared and `remove` left out, and
|
|
586
586
|
`prompt`: text the sessions on it get in their system prompt, verbatim (nil
|
|
587
587
|
inherits the base's, `""` clears it; at most 2000 characters, and one that
|
|
588
|
-
isn't a String or is longer is a load error).
|
|
588
|
+
isn't a String or is longer is a load error). `groups` selects the source
|
|
589
|
+
groups the toolkit reaches (names or globs, sorted and deduplicated; nil
|
|
590
|
+
inherits the base's, `[]` reaches none, and `:all` reaches every group).
|
|
591
|
+
Groups match against the tool index's load-time names. Users pick
|
|
589
592
|
it like chi's own (`chi --toolkit NAME`, `toolkit.default`, `/toolkit NAME`),
|
|
590
593
|
and the plugin with `ctx.use_toolkit` (below). Names are `a-z`, `0-9`, `_` and
|
|
591
594
|
`-`; one chi has (`full`, `lean`, `child`) or another bundle declared is a
|
|
@@ -593,11 +596,13 @@ load error. A session saved on a toolkit whose bundle is gone declares every
|
|
|
593
596
|
tool (full) until it comes back, with a warning at its first turn. See
|
|
594
597
|
[Toolkits](configuration.md#toolkits). `prompt:` needs chi 0.72.0 (an older
|
|
595
598
|
chi refuses the keyword, a load error: a bundle that passes it sets
|
|
596
|
-
`requires_chi: ">= 0.72.0"`).
|
|
599
|
+
`requires_chi: ">= 0.72.0"`). `groups:` needs chi 0.121.0: an older chi refuses that keyword
|
|
600
|
+
too, so a bundle passing it sets `requires_chi: ">= 0.121.0"`.
|
|
597
601
|
|
|
598
602
|
```ruby
|
|
599
603
|
chi.toolkit "review", base: "lean", add: %w[delegate delegate_result]
|
|
600
604
|
chi.toolkit "coding", base: "child", prompt: "- Be concise in your responses"
|
|
605
|
+
chi.toolkit "web", base: "lean", groups: %w[chrome db*]
|
|
601
606
|
```
|
|
602
607
|
|
|
603
608
|
### `chi.tool_source(call:, acts_as:, label:, wait:, prefix:)`
|
|
@@ -611,12 +616,19 @@ first source's `label`). The index has one group of its own, **`chi`**: the
|
|
|
611
616
|
built-ins the session's toolkit leaves out but doesn't remove
|
|
612
617
|
([toolkits](configuration.md#toolkits)), each with its registered schema.
|
|
613
618
|
The two tools are registered once, after the plugins load, and declared per
|
|
614
|
-
toolkit
|
|
615
|
-
|
|
619
|
+
toolkit when it reaches a source group or has a reachable built-in (`full`
|
|
620
|
+
with no source declares neither). Nothing a
|
|
616
621
|
source publishes later changes the request's tools, only what a search
|
|
617
622
|
answers, so the prompt cache keeps: the search tool's description lists the
|
|
618
623
|
source groups as they were at load, and a `chi` line first, per toolkit.
|
|
619
|
-
The mcp bundle is the only source so far (a group per server).
|
|
624
|
+
The mcp bundle is the only source so far (a group per server). `groups:`
|
|
625
|
+
filters the groups find_tools searches, the source descriptions in the
|
|
626
|
+
request, and the MCP tools `call_tool` can reach. Its group snapshot is
|
|
627
|
+
filtered again at each toolkit change; new groups published after load do
|
|
628
|
+
not alter that prompt description. An unreached MCP tool call is refused
|
|
629
|
+
before the guardrails without starting the server; a deferred stub says to
|
|
630
|
+
switch to a toolkit that reaches its group. `/mcp` marks servers outside the
|
|
631
|
+
session's toolkit. A source's
|
|
620
632
|
group named `chi`, or named like an earlier group, is left out of search,
|
|
621
633
|
lookup and the description, with one warning per name.
|
|
622
634
|
|
|
@@ -1377,8 +1389,12 @@ bundles:
|
|
|
1377
1389
|
its tools as `(failed: …)`. The saved list stays: the next session tries
|
|
1378
1390
|
again.
|
|
1379
1391
|
- **`start: eager`** on a server starts it with every session (in an init
|
|
1380
|
-
task, after the Bridge is up),
|
|
1381
|
-
|
|
1392
|
+
task, after the Bridge is up), even when that session's toolkit does not
|
|
1393
|
+
reach its group. A first-run init task still blocks the first turn until
|
|
1394
|
+
it finishes, even for an unreached group. A stale-cache refresh likewise
|
|
1395
|
+
starts its short-lived server regardless of the toolkit. Switching away
|
|
1396
|
+
from a group refuses its calls without starting a lazy cached server, and
|
|
1397
|
+
does not stop a server already running; switching back reuses it.
|
|
1382
1398
|
- **`npx -y …@latest` checks the npm registry on every start** (~3.4 s for
|
|
1383
1399
|
`chrome-devtools-mcp`, against ~0.9 s for the installed binary). The cache
|
|
1384
1400
|
hides it from new sessions, but not from the first call. For a faster first
|
data/docs/testing.md
CHANGED
|
@@ -230,7 +230,9 @@ A builder runs these while working, not the full suite: the specs the change tou
|
|
|
230
230
|
files it changed (`lib/samagotchi/foo/bar.rb` → `spec/foo/bar_spec.rb`, `bar_*_spec.rb`…), the specs that read
|
|
231
231
|
docs or source as text (its `RULES`: add one when a full run catches what the names missed) and the golden of a changed
|
|
232
232
|
file's layer (any `terminal_ui/*.rb` → `spec/terminal_ui_golden_spec.rb`; `turn_flow.rb` and `turn_note.rb`, whose
|
|
233
|
-
words the REPL prints, pick it by a rule)
|
|
233
|
+
words the REPL prints, pick it by a rule). Changes to `tool_declarations.rb` and `system_prompt.rb` also pick the
|
|
234
|
+
effective-model, request, prompt-snapshot and full system-prompt goldens because they hash the request's prompt and
|
|
235
|
+
tools. Then come the other name matches (a spec named after several changed files first, e.g. `engine_toolkit_spec.rb` for `engine.rb` and
|
|
234
236
|
`toolkit.rb`; then one beside the file, so `plugin/context.rb` picks `spec/samagotchi/plugin/*context*` before
|
|
235
237
|
`spec/guardrails/context_spec.rb`), then the specs that name a method the change touches (the innermost `def` around a
|
|
236
238
|
changed line, read with Prism; one `git grep` for up to 12 names; names under 4 characters, `initialize`, `to_s`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-peer
|
|
3
|
-
version: 0.4.
|
|
3
|
+
version: 0.4.4
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Pair this session with a Claude Code session you work with: the claude_send tool and /claude send it a part of the work (a plan step, a diff, a finding) to validate or as context, and Claude's answer comes back as the claude-outbox context; /claude install-skill installs Claude's chi-peer skill, which pairs"
|
|
6
6
|
trust_level: reviewed
|
|
@@ -12,7 +12,7 @@ plugin:
|
|
|
12
12
|
file: plugin.rb
|
|
13
13
|
sha256: sha256:50f427d3862ae9d0d15693b4d887c528162b96c0320cc663df89d59d830d4660
|
|
14
14
|
scripts:
|
|
15
|
-
pair: sha256:
|
|
15
|
+
pair: sha256:106d5b44020602fcd2d8065da30d10e1043e30c1b31ea82b4f27f7f79ada6592
|
|
16
16
|
chi-peer-SKILL.md: sha256:b9366f8f4aee9c54d209368ce421eab7b971c03c75efab0415d1988fd40f9e60
|
|
17
17
|
# The plugin needs Samagotchi::Plugin::Peer and chi.peer_source (chi 0.67.0).
|
|
18
18
|
# An agreeing answer's closing wake (the skills say an OK wakes chi once)
|
|
@@ -147,6 +147,6 @@ attach claude-outbox --why "Claude's answers to what you sent it (claude-peer)"
|
|
|
147
147
|
if [ -z "$inbox" ]; then
|
|
148
148
|
echo "paired with chi $id (socket $sock)"
|
|
149
149
|
else
|
|
150
|
-
echo "paired with chi $id (socket $inbox on chi's machine, forwarded
|
|
150
|
+
echo "paired with chi $id (socket $inbox on chi's machine, forwarded to this Claude's $sock)"
|
|
151
151
|
fi
|
|
152
152
|
$chi context ls "$id"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp
|
|
3
|
-
version: 0.7.
|
|
3
|
+
version: 0.7.2
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Tools from MCP servers (stdio): each server in config.yml bundles: mcp: servers: runs for the session; their tools go into chi's tool index: the model searches them with find_tools and calls one with call_tool (<server>/<tool>); /mcp lists them"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
files: {}
|
|
8
8
|
plugin:
|
|
9
9
|
file: plugin.rb
|
|
10
|
-
sha256: sha256:
|
|
10
|
+
sha256: sha256:c4bb4cf99ad43168ea68c25283a153c5dea1e5be123743c8a4c1278c16b5ea90
|
|
11
11
|
# 0.67.0 is the release with chi.tool_source (the core tool index the bundle publishes into).
|
|
12
|
-
requires_chi: ">= 0.
|
|
12
|
+
requires_chi: ">= 0.121.0"
|
|
@@ -354,6 +354,7 @@ class Plugin
|
|
|
354
354
|
def register(chi)
|
|
355
355
|
@chi = chi
|
|
356
356
|
ctx = chi.ctx
|
|
357
|
+
@ctx = ctx
|
|
357
358
|
configs = @settings["servers"]
|
|
358
359
|
configs = {} unless configs.is_a?(Hash)
|
|
359
360
|
unless configs.empty?
|
|
@@ -952,7 +953,10 @@ class Plugin
|
|
|
952
953
|
return "No servers. Add them in config.yml under `bundles: mcp: servers:` (docs/plugins.md, The mcp bundle)." if @servers.empty?
|
|
953
954
|
|
|
954
955
|
servers = @servers.map do |server|
|
|
955
|
-
|
|
956
|
+
marker = if @source && !@source.declared_groups.include?(server.name)
|
|
957
|
+
" (not in toolkit #{@ctx.toolkit})"
|
|
958
|
+
end
|
|
959
|
+
head = "**#{server.name}**: #{state_text(server)}#{marker}"
|
|
956
960
|
# Sorted: a server lists its tools in its own (often grouped) order.
|
|
957
961
|
tools = server.tools.sort_by(&:name).map { |t| ["- `#{t.server}/#{t.name}`", mark(t)].compact.join(" ") }
|
|
958
962
|
tools.empty? ? head : "#{head}\n#{tools.join("\n")}"
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -377,6 +377,7 @@ module Samagotchi
|
|
|
377
377
|
# A cut tool output is saved whole in the session's folder.
|
|
378
378
|
@kernel.session_dir = -> { @session&.id && Session.session_dir(@session.id, state_dir: session_state_dir) }
|
|
379
379
|
@kernel.guardrail_gate = @guardrail_wiring.gate
|
|
380
|
+
@kernel.participant_turn = -> { Steer.participant_author?(turn_author) }
|
|
380
381
|
# The turn-end warm-up; the kernel asks it for a slot pin per request.
|
|
381
382
|
@prompt_warmup = PromptWarmup.new
|
|
382
383
|
@kernel.warmup = @prompt_warmup
|
|
@@ -176,6 +176,11 @@ module Samagotchi
|
|
|
176
176
|
# The Guardrails::Gate ToolRunner asks before each call; the Engine sets
|
|
177
177
|
# it (nil: ToolRunner's own, hooks only).
|
|
178
178
|
attr_accessor :guardrail_gate
|
|
179
|
+
# Whether the current turn belongs to a participant, so pre-gate
|
|
180
|
+
# refusals can defer to ReviewBoundary.
|
|
181
|
+
attr_accessor :participant_turn
|
|
182
|
+
|
|
183
|
+
def participant_turn? = @participant_turn&.call == true
|
|
179
184
|
# @return [LLM::TurnSettings] the turn's vision, sampling, thinking and
|
|
180
185
|
# model name, set by the Engine per turn (#run sets the model name
|
|
181
186
|
# too); the thinking level is its own field, since the native path
|
|
@@ -308,12 +308,14 @@ module Samagotchi
|
|
|
308
308
|
# toolkit.default, ctx.use_toolkit): +base+'s tools (a toolkit chi or
|
|
309
309
|
# a bundle has) with +add+ declared and +remove+ left out, and
|
|
310
310
|
# +prompt+: text the sessions on it get in their system prompt (nil:
|
|
311
|
-
#
|
|
311
|
+
# its base's, "" none; at most Toolkit::PROMPT_MAX characters);
|
|
312
|
+
# +groups+: MCP source group names/globs (nil inherits its base).
|
|
313
|
+
# A
|
|
312
314
|
# name chi (full, lean, child) or another bundle has is a load error,
|
|
313
315
|
# and so is a prompt that isn't text or is too long; a base that
|
|
314
316
|
# doesn't resolve leaves the sessions on it with every tool
|
|
315
317
|
# (Toolkit#resolve).
|
|
316
|
-
def toolkit(name, base:, add: [], remove: [], prompt: nil)
|
|
318
|
+
def toolkit(name, base:, add: [], remove: [], prompt: nil, groups: nil)
|
|
317
319
|
name = name.to_s
|
|
318
320
|
raise ArgumentError, "toolkit name #{name.inspect} must be a-z, 0-9, _ and -" unless name.match?(TOOLKIT_NAME)
|
|
319
321
|
|
|
@@ -327,7 +329,8 @@ module Samagotchi
|
|
|
327
329
|
end
|
|
328
330
|
raise ArgumentError, "toolkit #{name} is declared twice" if @toolkits.any? { |kit| kit.name == name }
|
|
329
331
|
|
|
330
|
-
@toolkits << Toolkit::Kit.new(name: name, base: base, add: add, remove: remove, prompt: prompt,
|
|
332
|
+
@toolkits << Toolkit::Kit.new(name: name, base: base, add: add, remove: remove, prompt: prompt,
|
|
333
|
+
groups: groups, source: @bundle)
|
|
331
334
|
nil
|
|
332
335
|
end
|
|
333
336
|
|
|
@@ -15,6 +15,12 @@ module Samagotchi
|
|
|
15
15
|
# A numbered option shows as "1) Apple", a word as "yes: continue".
|
|
16
16
|
Option = Struct.new(:key, :label) do
|
|
17
17
|
def text = key.match?(/\A\d+\z/) ? "#{key}) #{label}" : "#{key}: #{label}"
|
|
18
|
+
|
|
19
|
+
# The model's pick (a label ending in "(Recommended)"): painted bold,
|
|
20
|
+
# its label kept as is so a typed label still matches.
|
|
21
|
+
def recommended? = label.to_s.rstrip.end_with?("(Recommended)")
|
|
22
|
+
|
|
23
|
+
def code = recommended? ? "1;92" : 92
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
CONTINUE_QUESTION = "The turn ran out of iterations. Continue it?"
|
|
@@ -83,7 +89,7 @@ module Samagotchi
|
|
|
83
89
|
question_rows = question_rows.first(1) unless details
|
|
84
90
|
rows.concat(question_rows.map { |row| @paint.call(row, @question_code) })
|
|
85
91
|
rows.concat(@details.map { |row| @paint.call(row, @question_code) }) if details
|
|
86
|
-
rows.concat(folded ? folded_options(width) : @options.map { |o| @paint.call(" #{o.text}",
|
|
92
|
+
rows.concat(folded ? folded_options(width) : @options.map { |o| @paint.call(" #{o.text}", o.code) })
|
|
87
93
|
rows << @paint.call(" [#{@hint}]", 90) if hint && @hint
|
|
88
94
|
rows
|
|
89
95
|
end
|
|
@@ -91,19 +97,24 @@ module Samagotchi
|
|
|
91
97
|
# The options packed into rows of +width+: " 1) Allow once · 2) Deny".
|
|
92
98
|
def folded_options(width)
|
|
93
99
|
rows = []
|
|
94
|
-
line =
|
|
100
|
+
line = []
|
|
95
101
|
@options.each do |option|
|
|
96
|
-
|
|
97
|
-
candidate = line.empty? ? " #{item}" : "#{line} · #{item}"
|
|
98
|
-
if !line.empty? && display_width(candidate) > width
|
|
102
|
+
if !line.empty? && display_width(" #{(line + [option]).map(&:text).join(" · ")}") > width
|
|
99
103
|
rows << line
|
|
100
|
-
line =
|
|
104
|
+
line = [option]
|
|
101
105
|
else
|
|
102
|
-
line
|
|
106
|
+
line << option
|
|
103
107
|
end
|
|
104
108
|
end
|
|
105
109
|
rows << line unless line.empty?
|
|
106
|
-
rows.map { |row|
|
|
110
|
+
rows.map { |row| paint_folded(row) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# One folded row; a recommended option in it is painted on its own.
|
|
114
|
+
def paint_folded(options)
|
|
115
|
+
return @paint.call(" #{options.map(&:text).join(" · ")}", 92) if options.none?(&:recommended?)
|
|
116
|
+
|
|
117
|
+
@paint.call(" ", 92) + options.map { |o| @paint.call(o.text, o.code) }.join(@paint.call(" · ", 92))
|
|
107
118
|
end
|
|
108
119
|
|
|
109
120
|
# Word wrap at +width+ columns, the first row after +first+, the others
|
|
@@ -412,7 +412,7 @@ module Samagotchi
|
|
|
412
412
|
},
|
|
413
413
|
{
|
|
414
414
|
name: "ask_user_question",
|
|
415
|
-
description: "Ask the user a structured qualification question. Supports single/multi selection plus optional freeform/Other input. Prefer this over plain numbered lists when you need a clear choice. The harness renders it natively and returns {selected, freeform}.",
|
|
415
|
+
description: "Ask the user a structured qualification question. Supports single/multi selection plus optional freeform/Other input. Prefer this over plain numbered lists when you need a clear choice. The harness renders it natively and returns {selected, freeform}. If you recommend one option, put it first and end its label with ' (Recommended)'.",
|
|
416
416
|
parameters: {
|
|
417
417
|
type: "object",
|
|
418
418
|
properties: {
|
|
@@ -77,7 +77,16 @@ module Samagotchi
|
|
|
77
77
|
# hook, no approval card for a call that can't run (KernelLoop#dispatch
|
|
78
78
|
# answers it with the not-loaded error).
|
|
79
79
|
original = call.dup
|
|
80
|
-
|
|
80
|
+
refusal = unreached_group_refusal(call)
|
|
81
|
+
verdict = if refusal
|
|
82
|
+
Guardrails::Verdict.new(call: call).deny!(refusal.message,
|
|
83
|
+
rule: Tools::ToolIndex::ToolkitGroupRefusal::RULE,
|
|
84
|
+
source: "core", decided_by: "core")
|
|
85
|
+
elsif left_out?(call)
|
|
86
|
+
Guardrails::Verdict.new(call: call)
|
|
87
|
+
else
|
|
88
|
+
evaluate(call, iteration, params)
|
|
89
|
+
end
|
|
81
90
|
call = verdict.call
|
|
82
91
|
params = ToolActivity.tool_activity_params(call[:name], call, registry: tools)
|
|
83
92
|
label = plugin_label(call[:name])
|
|
@@ -378,6 +387,27 @@ module Samagotchi
|
|
|
378
387
|
!registry.left_out(call[:name], layers: layers).nil?
|
|
379
388
|
end
|
|
380
389
|
|
|
390
|
+
def unreached_group_refusal(call)
|
|
391
|
+
return if participant_turn?
|
|
392
|
+
|
|
393
|
+
registry = tools
|
|
394
|
+
index = registry&.index if registry.respond_to?(:index)
|
|
395
|
+
return unless index.respond_to?(:unreached_tool)
|
|
396
|
+
|
|
397
|
+
name = if call[:name] == index.call_name
|
|
398
|
+
args = call[:args].is_a?(Hash) ? call[:args] : {}
|
|
399
|
+
args["tool"] || args[:tool]
|
|
400
|
+
else
|
|
401
|
+
call[:name]
|
|
402
|
+
end
|
|
403
|
+
tool = index.unreached_tool(name)
|
|
404
|
+
tool && Tools::ToolIndex::ToolkitGroupRefusal.for(tool, registry)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def participant_turn?
|
|
408
|
+
@kernel.respond_to?(:participant_turn?) && @kernel.participant_turn?
|
|
409
|
+
end
|
|
410
|
+
|
|
381
411
|
# Whether the turn's host loads tools (TurnSettings#loads_tools).
|
|
382
412
|
def loads_tools? = @kernel.respond_to?(:turn_settings) && @kernel.turn_settings&.loads_tools == true
|
|
383
413
|
|
|
@@ -431,6 +461,12 @@ module Samagotchi
|
|
|
431
461
|
# A legacy veto keeps its old text; a verdict's deny tells the model
|
|
432
462
|
# who decided and not to route around it.
|
|
433
463
|
def denied(call, verdict)
|
|
464
|
+
if verdict.rule == Tools::ToolIndex::ToolkitGroupRefusal::RULE
|
|
465
|
+
output = "[#{call[:name]}] #{verdict.reason}"
|
|
466
|
+
activity = ToolActivity.tool_activity_event(call[:name], call, output, registry: tools)
|
|
467
|
+
return { output: output, activity: activity.merge(status: "blocked", guardrail: verdict.to_activity) }
|
|
468
|
+
end
|
|
469
|
+
|
|
434
470
|
output = if verdict.legacy?
|
|
435
471
|
reason = verdict.reason.to_s.strip
|
|
436
472
|
"[#{call[:name]}] Error: blocked by guardrail: #{reason.empty? ? "blocked by hook" : reason}"
|