samagotchi 0.98.0 → 0.100.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 +40 -1
- data/README.md +2 -2
- data/docs/architecture.md +6 -3
- data/docs/bundle-authoring.md +1 -1
- data/docs/cli.md +1 -1
- data/docs/configuration.md +8 -0
- data/docs/guardrails.md +10 -2
- data/docs/hooks.md +9 -1
- data/docs/internals/anthropic.md +79 -8
- data/docs/internals/engine.md +13 -3
- data/docs/internals/llm-context-forget.md +2 -1
- data/docs/internals/prompt-caching.md +9 -0
- data/docs/memory.md +1 -1
- data/docs/plugins.md +83 -3
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +10 -0
- data/docs/sub-agent.md +7 -2
- data/docs/testing.md +27 -0
- data/lib/samagotchi/bundles/core/manifest.yml +4 -3
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/bundles/work-cap/manifest.yml +13 -0
- data/lib/samagotchi/bundles/work-cap/plugin.rb +391 -0
- data/lib/samagotchi/engine.rb +67 -9
- data/lib/samagotchi/hooks/loader.rb +17 -4
- data/lib/samagotchi/kernel_loop.rb +15 -2
- data/lib/samagotchi/llm/anthropic_messages.rb +85 -4
- data/lib/samagotchi/llm/anthropic_wire.rb +90 -11
- data/lib/samagotchi/llm/chat_loop.rb +76 -7
- data/lib/samagotchi/llm/dialect.rb +8 -5
- data/lib/samagotchi/llm/refusal_memo.rb +6 -0
- data/lib/samagotchi/llm/turn_settings.rb +13 -5
- data/lib/samagotchi/llm_context_stale.rb +2 -2
- data/lib/samagotchi/message_parts.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +2 -1
- data/lib/samagotchi/plugin/context.rb +31 -4
- data/lib/samagotchi/prompt.rb +3 -0
- data/lib/samagotchi/session_metrics/usage.rb +31 -0
- data/lib/samagotchi/session_metrics.rb +48 -5
- data/lib/samagotchi/testing/bundle.rb +2 -0
- data/lib/samagotchi/testing/host.rb +31 -1
- data/lib/samagotchi/testing/recorder.rb +18 -3
- data/lib/samagotchi/tool_loads.rb +62 -0
- data/lib/samagotchi/tool_response.rb +5 -1
- data/lib/samagotchi/tool_runner.rb +29 -7
- data/lib/samagotchi/tools/builtin_calls.rb +16 -0
- data/lib/samagotchi/tools/deferred_tools.rb +80 -0
- data/lib/samagotchi/tools/registry.rb +7 -2
- data/lib/samagotchi/tools/tool_index.rb +88 -6
- data/lib/samagotchi/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 10cafc8e1deb147528195213bd69b946f428918a8c344c57022b2c905cd2f4fb
|
|
4
|
+
data.tar.gz: 6b6764b06a4effc61ac2edd1d2221d7afd232b2a8ce3470f0b14280d3ded432d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4da078e7bb452a96fbc8d38f9565946fa12a3e3b8185e35f26032fcc2a3ae76249ec5ad7712dd12b74fec0f52db5fac7c03890f5074fca747985944b50f8e26
|
|
7
|
+
data.tar.gz: 3cc980b577869624c31a8c577b69612198569875d5c3b1e2f9070a4aebed2b5d4e6d4d29d44e25e56e2144b2346f4017352dca2e7a01b2898bc6fa5046b4459e
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,43 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.100.0] - 2026-10-22
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Plugins can read the session's work so far with `ctx.usage`: turns, tool calls (by tool), steps and active time
|
|
16
|
+
(generations plus tool calls, never a wait on a question), across workers, the same numbers `/stats` shows. The
|
|
17
|
+
bundle test kit sets it with `bundle.set(usage: {...})`. See [docs/plugins.md](docs/plugins.md).
|
|
18
|
+
- The `work-cap` bundle (in the `core` profile; `chi update` installs it): an optional per-session cap on tool calls
|
|
19
|
+
and active minutes across turns, the bound a session whose step-limit Continue keeps being answered otherwise
|
|
20
|
+
lacks. At a cap the turn pauses on Continue / Stop, the question naming the session's totals; `/workcap` shows or
|
|
21
|
+
sets it, `chi send --new --set work-cap.tool_calls=400` caps a child. Inert until a cap is set. See
|
|
22
|
+
[docs/plugins.md](docs/plugins.md#the-work-cap-bundle).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- An empty `hooks:` key in config.yml no longer crashes chi, and a malformed `hooks:` section (or one event's
|
|
27
|
+
entry) warns instead of being dropped without a word.
|
|
28
|
+
|
|
29
|
+
## [0.99.0] - 2026-10-22
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Sessions on Anthropic's Messages API (`api: anthropic`) can call a tool their toolkit leaves out by its own name:
|
|
34
|
+
the reachable built-ins (`list_sessions` on `lean`) and the MCP servers' tools (`mcp_fs_read`) go in each request
|
|
35
|
+
deferred (`defer_loading`), and a direct call runs under the same guardrails, approvals and rows as through
|
|
36
|
+
`call_tool`. A `find_tools` hit loads its matches by reference, so the model calls them by name and the request
|
|
37
|
+
after a load reads its whole prompt from the cache. Other hosts are unchanged. See
|
|
38
|
+
[docs/configuration.md](docs/configuration.md#toolkits).
|
|
39
|
+
- Plugins can load tools for the model with `ctx.load_tools(names)` (built-ins, MCP tools): on an Anthropic host the
|
|
40
|
+
model then calls them by name; elsewhere nothing is sent. A hint, not access: the guardrails stay. See
|
|
41
|
+
[docs/plugins.md](docs/plugins.md).
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- An Anthropic session whose toolkit leaves tools out, or that has MCP tools, may read its prompt again once on its
|
|
46
|
+
first turn after the upgrade (its request now carries the deferred tools).
|
|
47
|
+
|
|
11
48
|
## [0.98.0] - 2026-10-21
|
|
12
49
|
|
|
13
50
|
### Added
|
|
@@ -3755,7 +3792,9 @@ and long-lived sessions.
|
|
|
3755
3792
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3756
3793
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3757
3794
|
|
|
3758
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3795
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.100.0...HEAD
|
|
3796
|
+
[0.100.0]: https://github.com/dm1try/samagotchi/compare/v0.99.0...v0.100.0
|
|
3797
|
+
[0.99.0]: https://github.com/dm1try/samagotchi/compare/v0.98.0...v0.99.0
|
|
3759
3798
|
[0.98.0]: https://github.com/dm1try/samagotchi/compare/v0.97.0...v0.98.0
|
|
3760
3799
|
[0.97.0]: https://github.com/dm1try/samagotchi/compare/v0.96.0...v0.97.0
|
|
3761
3800
|
[0.96.0]: https://github.com/dm1try/samagotchi/compare/v0.95.0...v0.96.0
|
data/README.md
CHANGED
|
@@ -78,8 +78,8 @@ Messages API, picks the
|
|
|
78
78
|
model (or asks, when there are several), sends one test request and writes
|
|
79
79
|
the config. With a config already there, it adds a `hosts:` entry and keeps
|
|
80
80
|
the rest of the file. Then it installs the `core` bundles (loop-guard,
|
|
81
|
-
check-in, guardrails, slow-commands, agents-md, cost-cap: the safety set for a
|
|
82
|
-
small local model, the cost
|
|
81
|
+
check-in, guardrails, slow-commands, agents-md, cost-cap, work-cap: the safety set for a
|
|
82
|
+
small local model, the cost and work caps inert until you set one) and, on a terminal, offers `dev` (known-names, mcp, btw, skills,
|
|
83
83
|
source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes; see
|
|
84
84
|
[Bundle profiles](docs/memory.md#bundle-profiles-core-and-dev)). Edit the
|
|
85
85
|
file later as in [Configure](#configure); `chi bootstrap --help` has the
|
data/docs/architecture.md
CHANGED
|
@@ -411,7 +411,10 @@ stay the turns' only. `SessionMetrics#persist` is serialized (its own mutex, tak
|
|
|
411
411
|
threads' saves land in snapshot order.
|
|
412
412
|
A bundle's `ctx.cost` reads the same sums (`SessionMetrics#cost`, a frozen `SessionMetrics::Cost`): the `tokens`
|
|
413
413
|
block's `cost_sum` and `cost_estimate_sum` with the running turn's finished generations folded in, plus the `side`
|
|
414
|
-
block's, so it counts what earlier workers spent too ([plugins.md](plugins.md)).
|
|
414
|
+
block's, so it counts what earlier workers spent too ([plugins.md](plugins.md)). Its `ctx.usage` reads the work
|
|
415
|
+
(`SessionMetrics#usage`, a frozen `SessionMetrics::Usage`): the snapshot's `turns`, `tool_calls_total` /
|
|
416
|
+
`tool_calls_by_tool` and `iterations_total`, and `active_ms`, the generations' `gen_ms` plus the finished tool
|
|
417
|
+
records' `duration_ms` (tallied per tool as they are counted; `ask_user_question`'s, a wait on the user, left out).
|
|
415
418
|
|
|
416
419
|
The page's frequent reads of `GET /api/sessions/:id` stay light too:
|
|
417
420
|
- `?tail=1&recent=1` (each turn's end, a cancel, an answer display): `{tail, session: {id, status,
|
|
@@ -609,10 +612,10 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
609
612
|
| 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)). |
|
|
610
613
|
| 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)). |
|
|
611
614
|
| 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)). |
|
|
612
|
-
| 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/cost.rb` (`SessionMetrics::Cost`): the spend `ctx.cost` reads. `session_metrics/side_totals.rb` (`SessionMetrics::SideTotals`) and `side_usage.rb` (`SideUsage`): the side requests' sums, apart from the turns'. |
|
|
615
|
+
| 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/cost.rb` (`SessionMetrics::Cost`): the spend `ctx.cost` reads; `session_metrics/usage.rb` (`SessionMetrics::Usage`): the work `ctx.usage` reads. `session_metrics/side_totals.rb` (`SessionMetrics::SideTotals`) and `side_usage.rb` (`SideUsage`): the side requests' sums, apart from the turns'. |
|
|
613
616
|
| Images / vision | `image_store.rb` (`ImageStore`), `vision_support.rb`, `vision_context.rb` | Stored prompt images and whether a model takes them. |
|
|
614
617
|
| 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)). |
|
|
615
|
-
| 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)). |
|
|
618
|
+
| Tool index | `tools/tool_index.rb` (`Tools::ToolIndex`), `tools/deferred_tools.rb` | `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)). On `api: anthropic` they also go out deferred, a hit loads its matches and the model calls them by name ([internals/anthropic.md](internals/anthropic.md)). |
|
|
616
619
|
| Tool output cut | `tool_output_cut.rb` (`ToolOutputCut`), `tool_output_files.rb` (`ToolOutputFiles`) | A tool's output over `max_tool_output_chars`, cut by the tool's policy; an execute's whole output saved in the session's `tool_output/` folder ([internals/tool-guardrails.md](internals/tool-guardrails.md)). |
|
|
617
620
|
| Steer delivery | `steer.rb` (`Steer`), `delivery.rb` (`Delivery`), `steer_cut.rb` (`SteerCut`), `generation_phase.rb`, `waiting_steer.rb`, `steer_cut_after.rb` (`SteerCutAfter`), `cut_policy.rb` (`CutPolicy`) | A message for a running turn: at the next boundary, cutting a thinking-only generation, or queued for the turn's end ([internals/engine.md](internals/engine.md)). |
|
|
618
621
|
| Prompt files | `prompt_files.rb` (`PromptFiles`) | The AGENTS.md chain (root → cwd) and the appended files the system prompt carries. |
|
data/docs/bundle-authoring.md
CHANGED
|
@@ -129,7 +129,7 @@ class, a hand-built `ctx`), that is a gap in the kit: say so to the user rather
|
|
|
129
129
|
of a checksum mismatch, and a line with no sha installs unchecked. `chi bundle check --fix` rewrites them; run it
|
|
130
130
|
after the last edit.
|
|
131
131
|
- **requires_chi.** Use the chi the user runs (`chi --version`). `check` warns when the plugin calls something
|
|
132
|
-
newer than the floor (`ctx.http` needs 0.80.0, `ctx.cost` 0.83.0, `ctx.sessions.children` 0.34.0). A keyword added
|
|
132
|
+
newer than the floor (`ctx.http` needs 0.80.0, `ctx.cost` 0.83.0, `ctx.usage` 0.100.0, `ctx.sessions.children` 0.34.0). A keyword added
|
|
133
133
|
later it can't see: `ctx.ask_model(model:)` needs 0.80.0, `ctx.sessions.send(delivery:)` 0.64.0.
|
|
134
134
|
- **trust_level.** Leave it out. `reviewed` / `trusted` are for someone who reviewed the bundle, not its author.
|
|
135
135
|
- **Restart.** A session loads bundles when it starts: after an install or a settings change, the user starts a
|
data/docs/cli.md
CHANGED
|
@@ -84,7 +84,7 @@ chi bootstrap # try localhost 8080, 11434, 1234, 8000
|
|
|
84
84
|
or with anchors gets the lines printed to paste instead. `--dry-run` shows
|
|
85
85
|
what it would write.
|
|
86
86
|
- **The bundles.** Unless writing the config failed, it installs the system
|
|
87
|
-
bundle and the `core` profile (loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap), one line
|
|
87
|
+
bundle and the `core` profile (loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap, work-cap), one line
|
|
88
88
|
each; on a terminal it then asks `Also install dev (known-names, mcp, btw,
|
|
89
89
|
skills, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes)? [y/N]`. Run again, it installs nothing new: a bundle
|
|
90
90
|
you uninstalled stays out ([Bundle profiles](memory.md#bundle-profiles-core-and-dev)).
|
data/docs/configuration.md
CHANGED
|
@@ -1408,6 +1408,14 @@ toolkit:
|
|
|
1408
1408
|
starts no children). To keep a built-in away from the model, name it under `remove:`. `find_tools` and `call_tool`
|
|
1409
1409
|
are declared when a toolkit has a reachable built-in (or an MCP server is configured); `full` with no MCP server
|
|
1410
1410
|
declares neither, and removing them there (`--no-tool call_tool`) leaves nothing out: no prompt line, no chip.
|
|
1411
|
+
- **On `api: anthropic`, reachable tools are callable by name.** Every tool `call_tool` reaches (the reachable
|
|
1412
|
+
built-ins, the MCP servers' tools as `mcp_<server>_<tool>`) also goes in the request's tools deferred
|
|
1413
|
+
(`defer_loading`: Messages keeps them out of the model's view until something loads them, at a flat ~100 tokens).
|
|
1414
|
+
A direct call of one (`list_sessions`, `mcp_fs_read`) runs, under the same guardrails, approvals and rows as
|
|
1415
|
+
through `call_tool` (an MCP tool's call is `call_tool`'s). A `find_tools` hit **loads** its matches (up to 5):
|
|
1416
|
+
the model then sees their definitions and calls them by name; only the new result is read uncached. Not on
|
|
1417
|
+
Fireworks' dialect, nor on `api: openai` or a native host, where such a call gets the `call_tool` route and a hit
|
|
1418
|
+
reads as text.
|
|
1411
1419
|
- **The prompt line.** One line after the base prompt names them: `Not declared in this session (toolkit lean): ….
|
|
1412
1420
|
Search them with find_tools; call one with call_tool.`, then `Removed (toolkit child): memory_write, …. Only the
|
|
1413
1421
|
user can load one (/toolkit add <name>).` With `find_tools` removed, the first sentence ends `Call one with
|
data/docs/guardrails.md
CHANGED
|
@@ -125,7 +125,7 @@ answers it. Every question's options carry an **effect**, set by whoever asks:
|
|
|
125
125
|
- `deny`: Deny on an approval, Stop on the step-limit question or a plugin's
|
|
126
126
|
stop option;
|
|
127
127
|
- `spend`: more time and tokens, no new permission (Continue on the step-limit
|
|
128
|
-
question, cost-cap's Continue);
|
|
128
|
+
question, cost-cap's and work-cap's Continue);
|
|
129
129
|
- `permit`: one tool call (an approval's "Allow once");
|
|
130
130
|
- `permit_wide`: more than one call (this session, this repo or directory,
|
|
131
131
|
the rule).
|
|
@@ -140,7 +140,7 @@ guardrails:
|
|
|
140
140
|
- `hard`: `deny` and `answer` only. A parent can deny an approval
|
|
141
141
|
(`--option Deny --text "why"`, `--text` alone or `--dismiss`), Stop a
|
|
142
142
|
step-limit question and answer plain questions.
|
|
143
|
-
- `standard`: also `spend`: a parent may Continue a turn (or cost-cap's cap).
|
|
143
|
+
- `standard`: also `spend`: a parent may Continue a turn (or cost-cap's or work-cap's cap).
|
|
144
144
|
- `soft` (the default): also `permit`: `chi answer --option "Allow once"`
|
|
145
145
|
goes through, picked by the option's scope, not its label.
|
|
146
146
|
|
|
@@ -429,6 +429,14 @@ direct call, and an approval is keyed `delegate:…` either way. `tool:
|
|
|
429
429
|
call_tool` rules no longer see such calls, only MCP calls and misses. A
|
|
430
430
|
`chi/` name the toolkit removes is refused before the gate, with no card.
|
|
431
431
|
|
|
432
|
+
On a Messages host (`api: anthropic`) those tools are also callable by their
|
|
433
|
+
own names (they go out deferred): a direct `delegate` call is gated as any
|
|
434
|
+
declared tool's, keyed `delegate:…` (the same approval as through
|
|
435
|
+
`call_tool`); a direct call of an MCP tool's `mcp_<server>_<tool>` name is
|
|
436
|
+
gated as the `call_tool` call it stands for: `mcp_*` rules match it, and its
|
|
437
|
+
approval is `call_tool>mcp_x:…`, the same as the `call_tool` route's. A tool
|
|
438
|
+
the toolkit removes stays refused before the gate.
|
|
439
|
+
|
|
432
440
|
```yaml
|
|
433
441
|
- id: mcp-ask
|
|
434
442
|
# every MCP call (call_tool, acting as mcp_<server>_<tool>); not a find_tools search.
|
data/docs/hooks.md
CHANGED
|
@@ -26,6 +26,9 @@ hooks:
|
|
|
26
26
|
on_error: skip
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
Each event key holds a list of entries. A `hooks:` that isn't a mapping, or an event key that isn't a list
|
|
30
|
+
(`before_turn: guard.rb`), warns at each start and loads nothing for it; an empty key loads nothing, silently.
|
|
31
|
+
|
|
29
32
|
## Plugin Format
|
|
30
33
|
|
|
31
34
|
Each plugin is a `.rb` file in the hooks directory. The class name must match
|
|
@@ -610,6 +613,11 @@ A built-in reached through `call_tool` (`chi/delegate`, a tool the session's too
|
|
|
610
613
|
declare) fires as that built-in, its call built as a direct call's, with `call[:via] == "call_tool"`.
|
|
611
614
|
A hook that drops `via:` or rewrites the call to a tool the toolkit removes gets the call refused
|
|
612
615
|
by its name.
|
|
616
|
+
On a Messages host (`api: anthropic`), the model may call those tools by their own names (they
|
|
617
|
+
go out deferred, [configuration.md](configuration.md#toolkits)): a direct `delegate` fires as a
|
|
618
|
+
declared tool's call, with no `via:`; a direct `mcp_fs_read` fires as the `call_tool` call it
|
|
619
|
+
stands for (`tool: "call_tool"`, `args: {"tool" => "fs/read", "args" => {…}}`), as if the model
|
|
620
|
+
had called `call_tool`.
|
|
613
621
|
|
|
614
622
|
## Bundle Hooks (unified workflow bundle)
|
|
615
623
|
|
|
@@ -667,7 +675,7 @@ Notes:
|
|
|
667
675
|
- Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
|
|
668
676
|
- Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
|
|
669
677
|
- 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.
|
|
670
|
-
- 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)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Plugins](plugins.md#the-cost-cap-bundle)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Plugins](plugins.md#the-ruby-bundle)) `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)), `chi bundle install github-pr` (attached context: the branch's open PR, see [Plugins](plugins.md#the-github-pr-bundle)), `chi bundle install coordinator` (a plugin: parallel work in worktrees and child sessions, see [Plugins](plugins.md#the-coordinator-bundle)), `chi bundle install claude-peer` / `codex-peer` (a plugin: talk to a Claude Code or Codex session, see [Plugins](plugins.md#the-claude-peer-bundle)) and `chi bundle install model-notes` (memories: reviewed model notes, see [Plugins](plugins.md#the-model-notes-bundle)).
|
|
678
|
+
- 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)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Plugins](plugins.md#the-cost-cap-bundle)), `chi bundle install work-cap` (a plugin: a per-session cap on tool calls and active minutes, `/workcap`, see [Plugins](plugins.md#the-work-cap-bundle)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Plugins](plugins.md#the-ruby-bundle)) `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)), `chi bundle install github-pr` (attached context: the branch's open PR, see [Plugins](plugins.md#the-github-pr-bundle)), `chi bundle install coordinator` (a plugin: parallel work in worktrees and child sessions, see [Plugins](plugins.md#the-coordinator-bundle)), `chi bundle install claude-peer` / `codex-peer` (a plugin: talk to a Claude Code or Codex session, see [Plugins](plugins.md#the-claude-peer-bundle)) and `chi bundle install model-notes` (memories: reviewed model notes, see [Plugins](plugins.md#the-model-notes-bundle)).
|
|
671
679
|
- 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).
|
|
672
680
|
|
|
673
681
|
Lifecycle:
|
data/docs/internals/anthropic.md
CHANGED
|
@@ -13,7 +13,8 @@ ChatLoop::Run#generate
|
|
|
13
13
|
→ AnthropicMessages#chat
|
|
14
14
|
→ #request_body # options as Messages takes them, breakpoints on or off
|
|
15
15
|
→ AnthropicWire.translate # system blocks + messages, cache_control placed
|
|
16
|
-
→ AnthropicWire.tools # function schemas → {name, description, input_schema}
|
|
16
|
+
→ AnthropicWire.tools # function schemas → {name, description, input_schema},
|
|
17
|
+
# a deferred one with defer_loading: true
|
|
17
18
|
→ POST {base}/v1/messages # LLM::HTTP: retries, cancel, first-token limit
|
|
18
19
|
→ AnthropicAssembly#add per SSE event # → ChatResponse (text, reasoning, tool_calls, usage,
|
|
19
20
|
# finish_reason, provider_content)
|
|
@@ -35,6 +36,7 @@ Paths are under `lib/samagotchi/llm/`.
|
|
|
35
36
|
| `anthropic_wire.rb` | `AnthropicWire`: ChatLoop's wire messages as Messages' `system` and `messages`, the two breakpoints, tool ids and names |
|
|
36
37
|
| `anthropic_assembly.rb` | `AnthropicAssembly`: a response from a whole body (`.from_message`) or stream events (`#add`), its usage in the OpenAI shape |
|
|
37
38
|
| `dialect.rb` | `Dialect` (`STANDARD`, `FIREWORKS`): what a provider's `/v1/messages` does differently |
|
|
39
|
+
| `../tools/deferred_tools.rb` | `Tools::DeferredTools.list`: the tools a request declares deferred (Deferred tools) |
|
|
38
40
|
| `prompt_cache.rb` | `PromptCache.control`: the `cache_control` a breakpoint carries (`cache.ttl`) |
|
|
39
41
|
| `chat_loop.rb` | `ChatLoop#replay_blocks`, `#reasoning_history`'s floor, `Run#with_thinking` (saves `anthropic_content`), `#settle_dropped_thinking!` |
|
|
40
42
|
|
|
@@ -202,12 +204,76 @@ The log line's `cache=` says `on`, `1h` or nothing (`#cache_summary`). prompt-ca
|
|
|
202
204
|
|
|
203
205
|
`LLM::Dialect` is a host's `dialect:` (`Dialect.parse` warns once on an unknown name or on a host whose api isn't
|
|
204
206
|
`anthropic`): `auth` (the default key header), `thinking_display` (the `display` sent with adaptive or budget
|
|
205
|
-
thinking, nil for none), `cache_breakpoints` and `
|
|
206
|
-
|
|
207
|
-
| Dialect | auth | thinking display | breakpoints | session header |
|
|
208
|
-
|
|
209
|
-
| `STANDARD` (no `dialect:`) | `x-api-key` | `summarized` | yes | none |
|
|
210
|
-
| `FIREWORKS` | Bearer | none | no (an automatic prefix cache) | `x-session-affinity` |
|
|
207
|
+
thinking, nil for none), `cache_breakpoints`, `session_header` and `tool_loading` (deferred tools, below).
|
|
208
|
+
|
|
209
|
+
| Dialect | auth | thinking display | breakpoints | session header | tool loading |
|
|
210
|
+
|---|---|---|---|---|---|
|
|
211
|
+
| `STANDARD` (no `dialect:`) | `x-api-key` | `summarized` | yes | none | yes |
|
|
212
|
+
| `FIREWORKS` | Bearer | none | no (an automatic prefix cache) | `x-session-affinity` | no |
|
|
213
|
+
|
|
214
|
+
## Deferred tools
|
|
215
|
+
|
|
216
|
+
A session declares its toolkit's tools; the ones it reaches but doesn't declare (the built-ins its toolkit leaves out
|
|
217
|
+
without removing them, an MCP server's tools in the tool index) are found with `find_tools` and called with
|
|
218
|
+
`call_tool` on every host. On a Messages host they also go in `tools` with `defer_loading: true`, after the declared
|
|
219
|
+
ones: the model sees their names only when something loads them, and the API accepts a call of one by its own name.
|
|
220
|
+
`AnthropicMessages#loads_tools?(model)` says whether a request does this: the dialect's `tool_loading`, unless the
|
|
221
|
+
host refused it for that model.
|
|
222
|
+
|
|
223
|
+
`ChatLoop#tool_definitions(conversation, model:)` appends them (`Tools::DeferredTools.list`) only on such an
|
|
224
|
+
adapter, and only with at least one tool declared (Messages wants one). The list, per request:
|
|
225
|
+
|
|
226
|
+
1. the built-ins the toolkit reaches (`Registry#reachable_names`), in registry order;
|
|
227
|
+
2. the tool index's source tools while `call_tool` is declared (`ToolIndex#deferrable`, via
|
|
228
|
+
`Registry::Index#deferrable`): callable, the only tool with its acts_as, a name Messages takes, no registered
|
|
229
|
+
tool's name; by acts_as (`mcp_<server>_<tool>`), sorted by name;
|
|
230
|
+
3. every other name the conversation loaded (a find result's `loaded:`, a tool change's `add:`) that isn't declared,
|
|
231
|
+
as a stub ("Not available right now; find_tools finds it when it is.", an empty object schema): its server is
|
|
232
|
+
gone or still starting, and the reference in the history must name a tool in `tools`.
|
|
233
|
+
|
|
234
|
+
A deferred tool never carries `cache_control`, and the list sits after the declared tools, so the cached prefix holds
|
|
235
|
+
(prompt-caching.md). A session with nothing to defer (full, no MCP server) sends the bytes it always did.
|
|
236
|
+
|
|
237
|
+
### Loading by reference
|
|
238
|
+
|
|
239
|
+
A `find_tools` keyword hit loads its callable matches (`ToolIndex::FoundText#loaded`, at most `FIND_RESULTS`: a
|
|
240
|
+
built-in by its name, a source tool by its acts_as when it is deferrable; an empty query, a miss or the names listing
|
|
241
|
+
load nothing). `KernelLoop#dispatch` passes them on (`dispatched[:loaded]`), `ToolRun#loaded` carries them and
|
|
242
|
+
`ToolResponse.single` saves them on the result as `loaded:` (every chat host; `ToolResponse.joined` doesn't).
|
|
243
|
+
`ChatLoop#wire_messages` copies them onto the tool message only for an adapter that loads tools.
|
|
244
|
+
`AnthropicWire.translate(wire, cache:, deferred:)` takes the names this request declares deferred and renders a
|
|
245
|
+
result's `loaded:` names among them as a tool_result holding only
|
|
246
|
+
`{"type": "tool_reference", "tool_name": …}` blocks (Messages takes no other content beside a reference), followed by
|
|
247
|
+
a sibling text block: `Loaded: a, b. Call them directly by these names.`, then the result's text without its
|
|
248
|
+
`Call one with call_tool(…)` line (`ToolIndex::CALL_HINT`; the saved result keeps it). With that line and a trailing
|
|
249
|
+
"(call_tool works too)", Sonnet and Haiku 5.5 went through `call_tool` 2/2 live; without, 5/5 called the tool by
|
|
250
|
+
name. A result whose names aren't deferred here (declared since, or no tool loading) goes as plain text. The sibling
|
|
251
|
+
text is never marked; when breakpoint #2 falls on such a tool_result, it goes on its last reference.
|
|
252
|
+
|
|
253
|
+
### Tool changes (`ctx.load_tools`)
|
|
254
|
+
|
|
255
|
+
A plugin's `ctx.load_tools(names)` stages wire names (`Engine#load_tools`: `ToolIndex#deferred_name`, idempotent
|
|
256
|
+
against the staged names and the history's earlier changes, `ToolLoads`). `Engine#turn_messages` appends them after
|
|
257
|
+
the turn's user message (on a turn with none, after its trailing results or notes; a conversation ending with a reply
|
|
258
|
+
keeps them staged) as one `ToolLoads.entry` per bundle (`kind: "tool_change"`), only when the turn's host
|
|
259
|
+
`tool_changes?` (`AnthropicMessages#tool_changes?`: `loads_tools?`, unless the host refused changes for the model);
|
|
260
|
+
otherwise the staged names are dropped and logged (`tool_loads_not_sent`). `ChatLoop#wire_messages` sends the entry
|
|
261
|
+
only to such an adapter, `Prompt.format*` skips it. `AnthropicWire.translate` turns it into a `role: "system"` message
|
|
262
|
+
of its own, `{"type": "tool_addition", "tool": {"type": "tool_reference", "name": …}}` per name deferred in this
|
|
263
|
+
request (none: no message; note `name` here, `tool_name` in a result's reference), held until the run of user-role
|
|
264
|
+
messages it sits in ends: before the next assistant message, or last. `AnthropicMessages` adds
|
|
265
|
+
`INLINE_TOOLS_BETA` (`inline-tools-2026-09-15`) to `anthropic-beta` when the body has such a message, comma-joined with
|
|
266
|
+
the binding beta on a drop_block step. A 400 naming `tool_addition`, the beta or a system role (`TOOL_CHANGES_RE`)
|
|
267
|
+
sets `:tool_changes` for the model: the retry and every later request drop the system messages and the beta.
|
|
268
|
+
|
|
269
|
+
A direct call of a deferred tool runs whether something loaded it or not (the API doesn't check): the Engine sets
|
|
270
|
+
`TurnSettings#loads_tools` from the turn's final model (after the before_turn hooks), and then `ToolRunner#run` and
|
|
271
|
+
`KernelLoop#dispatch` take a reachable built-in by its own name (`Registry#indexed`) through the normal gate, and wrap
|
|
272
|
+
an MCP tool's acts_as into the `call_tool` call it stands for (`ToolIndex#wrap`): `call_tool`'s row, rules and
|
|
273
|
+
approvals. A reload rebuilds that wrapped call from the saved one (`BuiltinCalls.wrap_saved`). Each such call is
|
|
274
|
+
logged (`tool_direct_deferred`, with `loaded:` whether the conversation had loaded it). On `api: openai` and the
|
|
275
|
+
native loop nothing is deferred, and a direct call of a reachable built-in is refused with the `call_tool` route as
|
|
276
|
+
before.
|
|
211
277
|
|
|
212
278
|
## 400 fallbacks
|
|
213
279
|
|
|
@@ -225,9 +291,14 @@ client's life only):
|
|
|
225
291
|
| `cache_control` | no breakpoints | per host (`cache_control_refused`) |
|
|
226
292
|
| `max_tokens: N > M` (`OUTPUT_CAP_RE`) | `max_tokens` capped at M | per model (`output_cap`) |
|
|
227
293
|
| `block_binding` or the binding beta, on a drop_block retry (`thinking_binding_unsupported?`) | the strip instead (Preserved thinking) | per host (`binding_controls_refused`) |
|
|
294
|
+
| `tools.N.input_schema` (`TOOL_SCHEMA_RE`) where tool N is deferred | that tool leaves the deferred list (`call_tool` still reaches it); one fix per tool (the fix value is `[:tool_schema, name]`), at most `MAX_SCHEMA_FIXES` (5) per request; the names kept by `RefusalMemo#add` | per model (`tool_schema_refused`) |
|
|
295
|
+
| `tool_addition`, the inline-tools beta or a system role (`TOOL_CHANGES_RE`) on a request with a tool change | no system messages and no beta from then on (`#tool_changes?` false); the tools stay deferred and callable | per model (`tool_changes_refused`) |
|
|
296
|
+
| `defer_loading` or `tool_reference` (`TOOL_LOADING_RE`) on a request that carries either | no deferred tools from then on (`#loads_tools?` false, loads go as text); a direct call of a reachable tool is refused again from the next turn (`TurnSettings#loads_tools`) | per model (`tool_loading_refused`) |
|
|
228
297
|
|
|
229
298
|
The display check comes before the adaptive one: its path (`thinking.adaptive.display`) names adaptive too. A context
|
|
230
299
|
overflow, a refused sampling field, a signature or a binding-controls 400 is not one of these (`#refusal_fix` keeps
|
|
231
300
|
excluding them, so `ADAPTIVE_REFUSED_RE` never reads a binding 400's "adaptive" as a form refusal): a signature 400
|
|
232
301
|
goes to the binding recovery above, the rest on as they are, to the loop's own handling (ChatLoop's sampling and
|
|
233
|
-
thinking-fields fallbacks) or to the turn's failure.
|
|
302
|
+
thinking-fields fallbacks) or to the turn's failure. A tool-loading fix's retry that breaks a replayed thinking
|
|
303
|
+
block's binding gets a signature 400 like any other request, and the binding recovery takes it. These refusals come on
|
|
304
|
+
the first request that carries the feature, normally a turn's first.
|
data/docs/internals/engine.md
CHANGED
|
@@ -34,7 +34,8 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
|
|
|
34
34
|
│ # resumed: true (only on a step-limit Continue: resumed: given),
|
|
35
35
|
│ # images (only when there are any), cut_after (the model's cut wait, s)}
|
|
36
36
|
│ # refresh_profile! (may ask the server's /props once)
|
|
37
|
-
│ # turn.settings = LLM::TurnSettings (vision, sampling, thinking; model_name
|
|
37
|
+
│ # turn.settings = LLM::TurnSettings (vision, sampling, thinking; model_name, loads_tools … are set in
|
|
38
|
+
│ # generate, after the hooks, from the final model),
|
|
38
39
|
│ # from one EffectiveModel; ThinkingNotices#turn_started keeps the level and says one the host can't do
|
|
39
40
|
│ # announces guardrail load failures, starts and awaits plugin init tasks, applies staged tools,
|
|
40
41
|
│ # then the session's toolkit (apply_toolkit!: Registry#toolkit, ToolIndex#refresh! for find_tools'
|
|
@@ -42,6 +43,8 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
|
|
|
42
43
|
│ # fires the :session_start hook on the Engine's first turn, then the :before_turn hook
|
|
43
44
|
│ # (a frozen copy of the history, the prompt, and resumed)
|
|
44
45
|
│ # turn_messages: system head + history + due reminders (:reminder_injected) + the user message
|
|
46
|
+
│ # + the plugins' staged tool loads (ctx.load_tools: a kind "tool_change" system entry per bundle,
|
|
47
|
+
│ # only when the turn's host takes tool changes; #append_tool_loads)
|
|
45
48
|
│ # Returns false when a Stop came during the probes or the init wait, else true
|
|
46
49
|
│
|
|
47
50
|
├── generate(turn, max_iterations:, max_tool_output_chars:, pending_input:)
|
|
@@ -218,7 +221,10 @@ ChatLoop#complete(messages:, max_iterations:, on_stream_event:, cancel_controlle
|
|
|
218
221
|
|
|
219
222
|
The differences from the native loop:
|
|
220
223
|
|
|
221
|
-
- The tools go in each request's `tools` field (`tool_definitions`), not in the system prompt.
|
|
224
|
+
- The tools go in each request's `tools` field (`tool_definitions(conversation, model:)`), not in the system prompt.
|
|
225
|
+
On an adapter that loads tools (`#loads_tools?`: `AnthropicMessages`), the tools the session reaches but doesn't
|
|
226
|
+
declare follow, `defer_loading: true` (`Tools::DeferredTools`; anthropic.md, "Deferred tools"); the Run keeps the
|
|
227
|
+
last request's deferred names and logs a call of one (`tool_direct_deferred`).
|
|
222
228
|
- The conversation is converted to the OpenAI wire format (`wire_messages`), for every chat host;
|
|
223
229
|
`AnthropicMessages` translates that again into Messages' `system` and content blocks (`LLM::AnthropicWire`).
|
|
224
230
|
- The first system message carries `cache_split:` (from `SystemPrompt#stable_length`, which the Engine passes in
|
|
@@ -266,11 +272,15 @@ One iteration with tool calls:
|
|
|
266
272
|
→ :tool_dispatch_started {iteration, call_count}
|
|
267
273
|
→ for each call: ToolRunner#run(call, iteration:, call_index:, call_count:,
|
|
268
274
|
on_stream_event:, max_tool_output_chars:)
|
|
275
|
+
→ under TurnSettings#loads_tools, a direct call of a source tool's acts_as (mcp_fs_read) is wrapped
|
|
276
|
+
into the call_tool call it stands for (Registry::Index#wrap, ToolIndex#wrap)
|
|
269
277
|
→ first, the call's entry unwraps it (Registry::Entry#unwrap: a call_tool of chi/<built-in> becomes
|
|
270
278
|
that built-in's call with via: "call_tool", ToolIndex#unwrap); the rest sees the real call
|
|
271
279
|
→ a tool the session's toolkit leaves out (Registry#left_out; with via:, one Registry#indexed doesn't
|
|
272
280
|
reach) skips the gate: no hooks, no card; KernelLoop#dispatch answers it (the call_tool route for a
|
|
273
|
-
reachable tool, the not-loaded error for a removed one)
|
|
281
|
+
reachable tool, the not-loaded error for a removed one). Under TurnSettings#loads_tools a reachable
|
|
282
|
+
built-in called by its own name is no such call: it runs through the gate, and KernelLoop#dispatch
|
|
283
|
+
takes it (#loaded_entry)
|
|
274
284
|
→ Guardrails::Gate#evaluate (the before_tool_call hooks, then the core checks)
|
|
275
285
|
→ :tool_call_started
|
|
276
286
|
→ an ask verdict: Gate#settle_ask (asks the user, records waited_ms)
|
|
@@ -38,7 +38,8 @@ lets the model forget its own tool outputs. The pieces:
|
|
|
38
38
|
- **Stubs** (`LLMContextView`): `(forgotten) <note>` on the first output of a call's forgets in a row, `(forgotten
|
|
39
39
|
with tN: see its note)` on the rest (a run of the same note, author and stamp, no other output between them),
|
|
40
40
|
` [restore: tN]` on a non-read with a stored id, then each kept range under `lines A-B kept:`. The entry keeps its
|
|
41
|
-
role, tool_call_id and images bookkeeping as for stale
|
|
41
|
+
role, tool_call_id and images bookkeeping as for stale, and a find_tools result's `loaded:`, so a Messages host
|
|
42
|
+
still gets the load's references (anthropic.md, "Deferred tools"); only the sibling text is the stub.
|
|
42
43
|
- **Offers**: `ContextStatus` (internals/context-telemetry.md).
|
|
43
44
|
- **Persistence**: the edits ride on the entries (plan D3), so `--resume`, rollbacks and forks keep the stubs and the
|
|
44
45
|
restores; `/stats` counts each applied batch's re-prefill.
|
|
@@ -99,6 +99,15 @@ right-hand column happens.
|
|
|
99
99
|
id: every chat request carries them, unless the host's dialect has none (`Dialect#cache_breakpoints`: `fireworks`,
|
|
100
100
|
an automatic prefix cache) or the host refused `cache_control` with a 400 (no breakpoints for that worker's life).
|
|
101
101
|
[anthropic.md](anthropic.md) has the details.
|
|
102
|
+
- On a Messages host the tools a session reaches but doesn't declare go in `tools` deferred (`defer_loading`), after
|
|
103
|
+
the declared ones, never marked. Their presence is a flat ~93 tokens of the cached prefix, whatever their number
|
|
104
|
+
or names: a request with none and one with some don't share a prefix (measured 2026-10-22: a full + MCP session
|
|
105
|
+
read nothing of the prefix main's chi had cached, then read its own), but the list may grow mid-session (an MCP
|
|
106
|
+
server's `list_changed`, a tool sorted in mid-list) without a re-read. A find_tools hit's load goes in its own
|
|
107
|
+
tool_result (`tool_reference` blocks) with the text beside it, rendered the same in every later request (a
|
|
108
|
+
loaded name stays deferred, a stub once its source is gone), so the request after a load reads the whole prefix;
|
|
109
|
+
the loaded definitions and the new result are the fresh part. A plugin's
|
|
110
|
+
`ctx.load_tools` only appends (a system `tool_addition` after the user message, never a change to `tools`).
|
|
102
111
|
- Side requests (idle recap, `/btw`) go through `IdleClient` with their own short system prompt. On a current
|
|
103
112
|
llama.cpp they don't evict the session (see the server settings below); chi never pins them to a slot.
|
|
104
113
|
|
data/docs/memory.md
CHANGED
|
@@ -387,7 +387,7 @@ profiles, meta bundles that install a set of them:
|
|
|
387
387
|
|
|
388
388
|
| profile | bundles |
|
|
389
389
|
|---|---|
|
|
390
|
-
| `core` | loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap: the recommended safety set |
|
|
390
|
+
| `core` | loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap, work-cap: the recommended safety set |
|
|
391
391
|
| `dev` | known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes |
|
|
392
392
|
|
|
393
393
|
`chi bootstrap` installs `core` and, on a terminal, offers `dev`. An existing
|
data/docs/plugins.md
CHANGED
|
@@ -631,6 +631,17 @@ and after a reload. `tool: call_tool` rules see MCP calls and misses only. A
|
|
|
631
631
|
`call_tool` itself is removed, is refused by its name before the guardrails
|
|
632
632
|
(no hook, no card).
|
|
633
633
|
|
|
634
|
+
**On a Messages host the model calls them by name too.** With `api:
|
|
635
|
+
anthropic` (not Fireworks' dialect), every tool `call_tool` reaches goes in
|
|
636
|
+
the request's tools deferred (`defer_loading`: its name only, until
|
|
637
|
+
something loads it): a built-in by its name, a source's tool by its
|
|
638
|
+
`acts_as` (`mcp_fs_read`; one with a refusal, an acts_as two tools share, or
|
|
639
|
+
a name another tool has stays out). A direct call of one runs whether
|
|
640
|
+
anything loaded it or not: a built-in as itself (as if declared), a
|
|
641
|
+
source's tool as the `call_tool` call it stands for, so its rules,
|
|
642
|
+
approvals, hooks and row are `call_tool`'s. Elsewhere such a call is
|
|
643
|
+
refused with the `call_tool` route, as before.
|
|
644
|
+
|
|
634
645
|
```ruby
|
|
635
646
|
@source = chi.tool_source(label: "mcp", prefix: "mcp_",
|
|
636
647
|
call: ->(tool, args, ctx) { run(tool, args, ctx) }, # the result, as a tool block's
|
|
@@ -740,6 +751,7 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
740
751
|
| `ctx.model` | the model the session runs on now: its resolved ref (`host:id`), right after `/model` too |
|
|
741
752
|
| `ctx.model_key` | that model's memory overlay key (`<name>.<key>.md`, what `memory_write current_model_only` writes) |
|
|
742
753
|
| `ctx.cost` | the session's spend so far, read now, as a frozen value: `total` (USD, reported + estimated, side requests included; 0.0 before any), `reported` (by providers, `usage.cost`), `estimated` (from `hosts.<name>.models` prices; a generation is one or the other, never both), `side` (recap, `/btw`, `ask_model`: their share of `total`) and `known?` (false while generations or side requests ran but none was reported or priced, so a limit on it can't be enforced; a session with some priced generations reads known, the unpriced ones counting 0). It includes the running turn's finished generations and what earlier workers of the session spent. Use its methods, not its class. Needs chi 0.83.0 |
|
|
754
|
+
| `ctx.usage` | the session's work so far, read now, as a frozen value: `turns` (finished ones and the running one), `tool_calls` and `tool_calls_by_tool` (every call, running ones included; canonical names: an alias counts under its tool, a `call_tool` call under the inner tool), `steps` (the loop rounds that dispatched tools, `/stats`' iterations) and `active_ms` (`active_minutes` too): work time, the generations' time plus the finished tool calls' time, never the waits between them (a question, an approval, the user's answer to `ask_user_question`), with `tool_ms_by_tool` its tool part by tool, so a waiting tool's time (`task_wait`) can be left out. Its counts are the ones `/stats` and `chi sessions stats` show, earlier workers' included. Use its methods, not its class. Needs chi 0.100.0 (guard it with `ctx.respond_to?(:usage)`) |
|
|
743
755
|
| `ctx.settings` | the bundle's settings, frozen: config.yml's `bundles: <bundle>:` with the session's own `--set` values laid over it ([Per-session settings](#per-session-settings)); the config alone until the session is bound (in `register` and `chi.ctx`) |
|
|
744
756
|
| `ctx.data_dir` | `$XDG_STATE_HOME/samagotchi/plugins/<bundle>/`, created on first use. A session's own state goes in `sessions/<id>.json` or `sessions/<id>/` there: chi removes it when the session is deleted, discarded or pruned |
|
|
745
757
|
| `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
|
|
@@ -759,6 +771,7 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
759
771
|
| `ctx.toolkit` | the name of the session's toolkit (`full` for a session from before toolkits); nil with no session |
|
|
760
772
|
| `ctx.declared_tools` | the tool names the session's next turn declares (its toolkit resolved, with its own adds and removes): ask this, not the name, whether a tool is there |
|
|
761
773
|
| `ctx.use_toolkit(name)` | put the session on toolkit `name` (yours from `chi.toolkit`, or chi's): saved, applied at the next turn's start and announced (`toolkit> toolkit: NAME from the next turn's start`, the web's tools chip follows). Only before the session's first model reply and outside a turn (the declared tools start every request, and a plugin doesn't ask first); otherwise it returns false and changes nothing: tell the user to run `/toolkit NAME`. An unknown name raises ArgumentError. Needs chi 0.66.0 |
|
|
774
|
+
| `ctx.load_tools(names)` | load tools the session reaches but doesn't declare, so the model calls them by name: built-ins (`"delegate"`, `"chi/delegate"`), MCP tools (`"mcp_fs_read"`, `"fs/read"`). Returns the names it staged (the wire names); a declared tool, one loaded before, a removed or unknown one is skipped (an unknown one logged), so calling it every turn is fine. From a `before_turn` hook it loads for that turn; anywhere else (mid-turn too) from the next turn's start. Call it from a hook: before the session's first turn its toolkit isn't applied yet and nothing resolves. **A hint, not access**: the tools keep their guardrails and approvals, and only a host that takes tool changes (`api: anthropic`, not Fireworks) gets one, as a system message after the user's (`[tools loaded: delegate (your-bundle)]` in the session, a notice line live); elsewhere nothing is sent or saved, and the tools stay reachable with `call_tool`. A hook may switch the model after you call it, so the return value can't promise delivery. Raises ArgumentError for anything but an Array. Needs chi 0.99.0 |
|
|
762
775
|
| `ctx.context` | attach outside text to this session and list it: see [Attached context](#attached-context-ctxcontext) |
|
|
763
776
|
|
|
764
777
|
The Engine itself is never handed to a plugin.
|
|
@@ -1108,7 +1121,7 @@ CHANGELOG line and bumps the shipped bundles that use it. Anything else under
|
|
|
1108
1121
|
|
|
1109
1122
|
**Stable**
|
|
1110
1123
|
|
|
1111
|
-
1. `Samagotchi::Plugin::Api` / `chi.*` and `ctx` as documented above (`ctx.cost`'s
|
|
1124
|
+
1. `Samagotchi::Plugin::Api` / `chi.*` and `ctx` as documented above (`ctx.cost`'s and `ctx.usage`'s values by their methods), including
|
|
1112
1125
|
`Samagotchi::Plugin::ToolResult`, `Samagotchi::Plugin::ModelError` (and its
|
|
1113
1126
|
subclass `Samagotchi::Plugin::ModelCancelled`),
|
|
1114
1127
|
`Samagotchi::Plugin::Service::Stopped`, `Samagotchi::Plugin::Sessions::Error`
|
|
@@ -1175,7 +1188,9 @@ bundle.sessions.on(:children) { |all:| [] } # ctx.sessions, scripte
|
|
|
1175
1188
|
bundle.http.stub(:post, "http://127.0.0.1:9000/classify", json: { label: "safe" }) # ctx.http, never the network
|
|
1176
1189
|
bundle.set(running: false) # the session's values: cwd:, messages:, cancelled: …
|
|
1177
1190
|
bundle.set(cost: 2.5) # ctx.cost: reported USD, or { reported:, estimated:, side:, known: }
|
|
1191
|
+
bundle.set(usage: { tool_calls: 40, active_ms: 600_000 }) # ctx.usage: { turns:, tool_calls:, tool_calls_by_tool:, steps:, active_ms:, tool_ms_by_tool: }
|
|
1178
1192
|
bundle.set(session_settings: { "cap" => 1 }) # the session's --set values (Bundle.load takes session_settings: too)
|
|
1193
|
+
bundle.tool_loads.last.names # ctx.load_tools calls; set(load_tools_staged: [...]) limits what they stage
|
|
1179
1194
|
bundle.shutdown
|
|
1180
1195
|
```
|
|
1181
1196
|
|
|
@@ -1209,6 +1224,10 @@ bundle.shutdown
|
|
|
1209
1224
|
- **`ctx.cost`** is zero and known unless the test sets it: `bundle.set(cost: 2.5)` (reported USD), a Hash of the
|
|
1210
1225
|
parts (`reported:`, `estimated:`, `side:`, `known:`; `total` is reported + estimated) or a callable giving either,
|
|
1211
1226
|
called at each read (a counting lambda shows whether the bundle read it).
|
|
1227
|
+
- **`ctx.usage`** is zero unless the test sets it: `bundle.set(usage: {...})`, a Hash of the parts (`turns:`,
|
|
1228
|
+
`tool_calls:`, `tool_calls_by_tool:`, `steps:`, `active_ms:`, `tool_ms_by_tool:`; zeros for the rest, an unknown
|
|
1229
|
+
part raises; `tool_calls` defaults to the sum of `tool_calls_by_tool`, `active_ms` to the sum of
|
|
1230
|
+
`tool_ms_by_tool`) or a callable giving one, called at each read.
|
|
1212
1231
|
- **`session_settings:`** (on `Bundle.load` and `bundle.set`) are the session's own `--set` values for the bundle,
|
|
1213
1232
|
keys its manifest lists in `session_settings:` (another key, or a value of another type than the manifest gives
|
|
1214
1233
|
it, raises `ArgumentError`, as chi refuses it). `ctx.settings`
|
|
@@ -1276,7 +1295,9 @@ by one: the bundle publishes them into chi's tool index
|
|
|
1276
1295
|
([chi.tool_source](#chitool_sourcecall-acts_as-label-wait-prefix), a group
|
|
1277
1296
|
per server), and the model has its two tools, `find_tools` and `call_tool` (~250 tokens in
|
|
1278
1297
|
every request plus a line per server, whatever the servers have; `/mcp`
|
|
1279
|
-
says how many).
|
|
1298
|
+
says how many). On `api: anthropic` each tool also goes out deferred as
|
|
1299
|
+
`mcp_<server>_<tool>`, so a `find_tools` hit loads it and the model calls it
|
|
1300
|
+
by that name, as `call_tool` would run it. Stdio servers only, for now.
|
|
1280
1301
|
|
|
1281
1302
|
```yaml
|
|
1282
1303
|
# config.yml
|
|
@@ -1665,6 +1686,64 @@ its own card and doesn't wake its parent ([sessions.md](sessions.md#delegating))
|
|
|
1665
1686
|
The state is one file per session, `<data_dir>/sessions/<session id>.json` (`{"cap": 3.0, "step": 3.0}`), removed
|
|
1666
1687
|
with the session.
|
|
1667
1688
|
|
|
1689
|
+
Its sibling for work instead of money, tool calls and active minutes across the session's turns, is
|
|
1690
|
+
[the work-cap bundle](#the-work-cap-bundle): the cap a local model (no cost) can have.
|
|
1691
|
+
|
|
1692
|
+
## The work-cap bundle
|
|
1693
|
+
|
|
1694
|
+
`chi bundle install work-cap` installs the bundle shipped with chi (`chi bootstrap` installs it with the `core`
|
|
1695
|
+
profile; an existing install gets it with `chi update`). It caps the work one session does across its turns: tool
|
|
1696
|
+
calls and active minutes. The per-turn meters reset every turn (the step limit, check-in, loop-guard), and a
|
|
1697
|
+
Continue on the step-limit question starts the next 300 steps; a parent agent may answer that Continue itself
|
|
1698
|
+
(`chi answer`), so a child it keeps continuing has no overall bound but cost-cap's `$`, and on a local model none.
|
|
1699
|
+
work-cap is that bound, with cost-cap's shape: the harness watches, the model never sees any of it. It is inert
|
|
1700
|
+
without a cap, and there is none by default.
|
|
1701
|
+
|
|
1702
|
+
```yaml
|
|
1703
|
+
# config.yml
|
|
1704
|
+
bundles:
|
|
1705
|
+
work-cap:
|
|
1706
|
+
child: {tool_calls: 500, minutes: 90} # delegate children with no own value
|
|
1707
|
+
default: {} # every other session (and a delegate without child:); empty: no cap
|
|
1708
|
+
warn_at: 0.8 # tell the user (only) once at this share of a cap; 0/off: no warning
|
|
1709
|
+
ignore_tools: [task_wait, task_get, delegate, delegate_result] # not counted: waiting
|
|
1710
|
+
```
|
|
1711
|
+
|
|
1712
|
+
- **What counts** (`ctx.usage`): **tool calls**, the session's own across its turns and workers, less
|
|
1713
|
+
`ignore_tools` (the waiting tools by default, `delegate` among them since it blocks on its child, so a session
|
|
1714
|
+
waiting on a long task or a child isn't capped for waiting); and **active minutes**, work time: the generations' time plus the finished tool calls' time (less
|
|
1715
|
+
`ignore_tools`' time), never a wait (a question, its own included, an approval, the user's answer to
|
|
1716
|
+
`ask_user_question`), so idle time and a Continue answered hours later don't count.
|
|
1717
|
+
- **When it checks:** before each generation, like cost-cap: the overshoot is at most one generation's tool calls.
|
|
1718
|
+
When the step limit and a work cap fall in the same turn, the work cap asks first, and its Stop ends the turn.
|
|
1719
|
+
- **At `warn_at` of a cap** you get one notice per meter, `work-cap> 320 of 400 tool calls (80%)`; a raised cap
|
|
1720
|
+
warns again.
|
|
1721
|
+
- **At a cap** the turn pauses on a question (header "Work cap") that names the session's totals, so a parent
|
|
1722
|
+
answering it decides on the whole picture: `Work cap reached: 412 tool calls of 400 (6 turns, 47 min active,
|
|
1723
|
+
$1.20). Continue raises it to 800.` **Continue** raises the cap by another cap's worth (as many steps as it
|
|
1724
|
+
takes to pass the work, saved for the session), **Stop** stops the turn (`canceled` with `stopped_by: work-cap`)
|
|
1725
|
+
with a card showing the totals. Both meters at once: one question naming both, Continue raises each. With no one
|
|
1726
|
+
to ask (`--non-interactive`) it stops. Continue is tagged `:spend`, as cost-cap's: a parent agent may Continue
|
|
1727
|
+
under `guardrails.parent_answers: soft` or `standard`, only Stop under `hard` (see
|
|
1728
|
+
[Guardrails](guardrails.md#answers-from-a-parent-agent)).
|
|
1729
|
+
- **The caps of a session**, each meter on its own: the one `/workcap` or a Continue set, else its own value (`chi
|
|
1730
|
+
--set work-cap.tool_calls=400`, `work-cap.minutes=60`, `chi send --new --set …`; `off`, `0` or a negative number
|
|
1731
|
+
for none), else `child:`'s for a delegate child, else `default:`'s. Minutes in config.yml read `90`, `90m` or
|
|
1732
|
+
`1.5h`. A value that doesn't read (`tool_calls: many`, a `default:` that isn't a mapping) gets one notice,
|
|
1733
|
+
`work-cap: tool_calls: "many" isn't a number or off; using default.tool_calls: (400)`, and the next value is used,
|
|
1734
|
+
never a silent no cap.
|
|
1735
|
+
|
|
1736
|
+
**`/workcap`** (works while a turn runs; the next generation reads it):
|
|
1737
|
+
|
|
1738
|
+
- `/workcap`: `work-cap: 320 of 400 tool calls (6 turns, 47 min active, $1.20)`, or `work-cap: no cap (…)` with the
|
|
1739
|
+
totals.
|
|
1740
|
+
- `/workcap 400`: the tool-call cap (and what Continue adds) becomes 400 for this session; `/workcap 90m` or
|
|
1741
|
+
`/workcap 2h`: the active-minutes cap.
|
|
1742
|
+
- `/workcap off` (or `/workcap 0`): no work cap for this session, over the config.
|
|
1743
|
+
|
|
1744
|
+
The state is one file per session, `<data_dir>/sessions/<session id>.json` (`{"tool_calls": {"cap": 800, "step":
|
|
1745
|
+
400}}`, a meter's `"cap": null` for off).
|
|
1746
|
+
|
|
1668
1747
|
## The check-in bundle
|
|
1669
1748
|
|
|
1670
1749
|
`chi bundle install check-in` installs the bundle shipped with chi (`chi
|
|
@@ -1697,7 +1776,8 @@ what it has found. check-in counts the turn's tool calls, and when there are
|
|
|
1697
1776
|
- **`mode: notify`**: a notice line only.
|
|
1698
1777
|
|
|
1699
1778
|
The count is per turn: a new turn (a prompt, a continue, a reminder) starts
|
|
1700
|
-
from zero; steering merged into the running turn doesn't reset it
|
|
1779
|
+
from zero; steering merged into the running turn doesn't reset it (a cap
|
|
1780
|
+
across the session's turns is [the work-cap bundle](#the-work-cap-bundle)). The polling
|
|
1701
1781
|
tools are not counted. A nudge that arrives after the model's final answer is
|
|
1702
1782
|
dropped (logged), never restarting a turn that is done; the card's "Nudged the
|
|
1703
1783
|
model at N tool calls" then becomes "The answer came first; nudge not sent."
|
data/docs/releasing.md
CHANGED
|
@@ -13,7 +13,7 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
13
13
|
and `rake release:check` enforce it). The system bundle upgrades itself when
|
|
14
14
|
chi starts.
|
|
15
15
|
- **Every other shipped bundle** (btw, check-in, guardrails, known-names,
|
|
16
|
-
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, and the profiles core and dev) has its own semver in its
|
|
16
|
+
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, and the profiles core and dev) has its own semver in its
|
|
17
17
|
`manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
|
|
18
18
|
`chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
|
|
19
19
|
- a change to a bundle's files bumps that bundle's `version:`
|
data/docs/sessions.md
CHANGED
|
@@ -253,6 +253,16 @@ declares every tool (full). The file stores only the name: a kit's tools and pro
|
|
|
253
253
|
`/toolkit` changes it, saved at once, from the next turn's start. See
|
|
254
254
|
[Toolkits](configuration.md#toolkits).
|
|
255
255
|
|
|
256
|
+
A `find_tools` result on a chat host saves the tools its hit loads as `loaded: ["list_sessions", "mcp_fs_read"]` on
|
|
257
|
+
its `tool_response` entry (wire names: a built-in's, an MCP tool's `mcp_<server>_<tool>`), whatever the host: a
|
|
258
|
+
later turn on `api: anthropic` (after a `/model` switch, a resume) sends the load as references, any other host
|
|
259
|
+
ignores it. The names stay deferred for the session's life, across respawns, `--resume` and forks.
|
|
260
|
+
|
|
261
|
+
A plugin's `ctx.load_tools` leaves a system entry `{"role": "system", "kind": "tool_change", "content": "[tools loaded:
|
|
262
|
+
delegate (bundle)]", "tool_change": {"add": ["delegate"], "by": "bundle"}}` after the turn's user message, only on a
|
|
263
|
+
host that takes tool changes (`api: anthropic`). It is never the system head, counts as conversation, is hidden in
|
|
264
|
+
the web and TUI histories, and goes to no other wire; its names stay deferred for the session's life like a load's.
|
|
265
|
+
|
|
256
266
|
## Parent answers
|
|
257
267
|
|
|
258
268
|
A session file's `parent_answers` (`hard`, `standard`, `soft`, or `null` for none) is the session's own
|