samagotchi 0.97.0 → 0.99.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 +50 -1
- data/docs/architecture.md +11 -3
- data/docs/bundle-authoring.md +5 -3
- data/docs/cli.md +8 -3
- data/docs/configuration.md +8 -0
- data/docs/guardrails.md +8 -0
- data/docs/hooks.md +64 -0
- data/docs/internals/anthropic.md +79 -8
- data/docs/internals/bundles.md +1 -1
- data/docs/internals/engine.md +31 -3
- data/docs/internals/llm-context-forget.md +2 -1
- data/docs/internals/prompt-caching.md +9 -0
- data/docs/plugins.md +24 -2
- data/docs/sessions.md +11 -1
- data/docs/testing.md +27 -0
- data/lib/samagotchi/answer_refs.rb +16 -9
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +39 -5
- data/lib/samagotchi/bundles/source-links/manifest.yml +5 -5
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +91 -10
- data/lib/samagotchi/hooks/bundle_loader.rb +18 -15
- data/lib/samagotchi/hooks.rb +4 -1
- 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/errors.rb +51 -17
- 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/memory_bundle/bundle_hook.rb +28 -9
- data/lib/samagotchi/memory_bundle/manifest.rb +17 -0
- data/lib/samagotchi/message_parts.rb +2 -1
- data/lib/samagotchi/output_formatter.rb +8 -2
- data/lib/samagotchi/pending_question.rb +7 -3
- data/lib/samagotchi/plugin/api.rb +6 -1
- data/lib/samagotchi/plugin/context.rb +21 -2
- data/lib/samagotchi/prompt.rb +3 -0
- data/lib/samagotchi/question_asked.rb +83 -0
- data/lib/samagotchi/question_desk.rb +3 -2
- data/lib/samagotchi/testing/bundle.rb +20 -1
- data/lib/samagotchi/testing/host.rb +1 -0
- data/lib/samagotchi/testing/recorder.rb +13 -2
- 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
- data/lib/samagotchi/web/app.rb +30 -9
- data/lib/samagotchi/web/public/app.js +44 -11
- data/lib/samagotchi/web/public/index.html +36 -13
- data/lib/samagotchi/web/public/info_bar_model.js +0 -2
- data/lib/samagotchi/web/public/lists_view.js +32 -8
- data/lib/samagotchi/web/public/markdown_place.js +25 -8
- data/lib/samagotchi/web/public/question_cards.js +23 -6
- data/lib/samagotchi/web/public/session_rename.js +88 -15
- data/lib/samagotchi/web/public/session_title.js +6 -0
- data/lib/samagotchi/web/public/stage_frame.js +24 -14
- data/lib/samagotchi/web/public/stage_model.js +4 -2
- data/lib/samagotchi/web/public/stage_slots.js +41 -9
- data/lib/samagotchi/web/public/stage_view.js +149 -8
- data/lib/samagotchi/web/public/stream_handlers.js +5 -1
- data/lib/samagotchi/web/public/turn_model.js +20 -0
- data/lib/samagotchi/web/public/turn_view.js +22 -9
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0185e889922200db9f6882d899b3ff17c74d296aa8c7823a7040e45e308edc47'
|
|
4
|
+
data.tar.gz: addddd537eac6fc703d0e217c7a968446ca44bb53f8ee501e61de86b1163085c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 62a2aaff51d6bbc45cfcc59e3d199444448cd838c229fbe4bda717658b6ba83fc7f273326ea0d8e8cb3e34839bd3c78ba472a5792589dbfe87a5dd1a8df2c5a7
|
|
7
|
+
data.tar.gz: 8b68d54a05c89461dba2428496111897cf85ca1cc70ac70f2b1621edcce7d70908243e181644b7086557d347fc80c51305a7b3df446c9299a09c2ec8e1cd113b
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,53 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.99.0] - 2026-10-22
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Sessions on Anthropic's Messages API (`api: anthropic`) can call a tool their toolkit leaves out by its own name:
|
|
16
|
+
the reachable built-ins (`list_sessions` on `lean`) and the MCP servers' tools (`mcp_fs_read`) go in each request
|
|
17
|
+
deferred (`defer_loading`), and a direct call runs under the same guardrails, approvals and rows as through
|
|
18
|
+
`call_tool`. A `find_tools` hit loads its matches by reference, so the model calls them by name and the request
|
|
19
|
+
after a load reads its whole prompt from the cache. Other hosts are unchanged. See
|
|
20
|
+
[docs/configuration.md](docs/configuration.md#toolkits).
|
|
21
|
+
- Plugins can load tools for the model with `ctx.load_tools(names)` (built-ins, MCP tools): on an Anthropic host the
|
|
22
|
+
model then calls them by name; elsewhere nothing is sent. A hint, not access: the guardrails stay. See
|
|
23
|
+
[docs/plugins.md](docs/plugins.md).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- An Anthropic session whose toolkit leaves tools out, or that has MCP tools, may read its prompt again once on its
|
|
28
|
+
first turn after the upgrade (its request now carries the deferred tools).
|
|
29
|
+
|
|
30
|
+
## [0.98.0] - 2026-10-21
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- A long question from the model gets room in the web stage: when its reply doesn't fit, the stage grows (up to 85%
|
|
35
|
+
of the panel) for the turn, the reply reads from its start in one scroll, and an "Answer ↓" pill leads to the card
|
|
36
|
+
while it is out of view. Approvals, the step limit and hooks' questions keep their layout.
|
|
37
|
+
- A new hook event, `:question_asked`, lets a plugin attach refs to the reply that asks a question or an approval
|
|
38
|
+
(display only, never in the prompt); the web pairs them in the asking text, on the card and in the history step.
|
|
39
|
+
A bundle manifest's hook can list `events: [...]` to run one hook for several events. See
|
|
40
|
+
[docs/hooks.md](docs/hooks.md).
|
|
41
|
+
- source-links 0.6.0 links the issue keys and paths in a waiting question's reply too, not only in answers.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- The web's rename ✎ moved from the info bar to the session's title: on the active card and next to the title in
|
|
46
|
+
the top bar, with the input over the title.
|
|
47
|
+
- A model server's 404 that lists its models puts `did you mean …?` ahead of the (cut) list, and names up to three
|
|
48
|
+
ids: every nearest typo on a tie, or the listed ids that contain the name asked for (`Qwen3.6-35B` →
|
|
49
|
+
`mlx-community--Qwen3.6-35B-A3B-4bit` or `…-8bit`).
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- A Gemma 4 step on the native loop no longer shows its tool call's body (`call:ask_user_question{…}`) as text in
|
|
54
|
+
the web history after a reload.
|
|
55
|
+
|
|
56
|
+
Update with `chi update` (the system bundle 0.98.0, source-links 0.6.0, the dev profile 0.7.1), then restart `chi web`.
|
|
57
|
+
|
|
11
58
|
## [0.97.0] - 2026-10-21
|
|
12
59
|
|
|
13
60
|
### Added
|
|
@@ -3727,7 +3774,9 @@ and long-lived sessions.
|
|
|
3727
3774
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3728
3775
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3729
3776
|
|
|
3730
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3777
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.99.0...HEAD
|
|
3778
|
+
[0.99.0]: https://github.com/dm1try/samagotchi/compare/v0.98.0...v0.99.0
|
|
3779
|
+
[0.98.0]: https://github.com/dm1try/samagotchi/compare/v0.97.0...v0.98.0
|
|
3731
3780
|
[0.97.0]: https://github.com/dm1try/samagotchi/compare/v0.96.0...v0.97.0
|
|
3732
3781
|
[0.96.0]: https://github.com/dm1try/samagotchi/compare/v0.95.0...v0.96.0
|
|
3733
3782
|
[0.95.0]: https://github.com/dm1try/samagotchi/compare/v0.94.0...v0.95.0
|
data/docs/architecture.md
CHANGED
|
@@ -421,7 +421,14 @@ The page's frequent reads of `GET /api/sessions/:id` stay light too:
|
|
|
421
421
|
finishes the ended turn's own line by its `turn_id` (sent as `&turn_id=`, which also picks that
|
|
422
422
|
turn's answer). A tail read's timing is always the newest turn's; `recent=1` is accepted and not needed.
|
|
423
423
|
The page binds an answer's `display_refs` to its bubble (`answer_refs.js`: on a reload's render and on this
|
|
424
|
-
read's re-render) and pairs them with the bubble's links and text before the `path:line` scan. A
|
|
424
|
+
read's re-render) and pairs them with the bubble's links and text before the `path:line` scan. A question's
|
|
425
|
+
refs (`pending_question.refs`, `:question_asked`) pair the same way (`markdown_place.js` `linkRefsIn`/`pairRefsIn`):
|
|
426
|
+
`question_requested` (or a snapshot's pending question) keeps them on the asking step's gen
|
|
427
|
+
(`turn_model.js` `questionRefs`) before the card draws, so the stage's asking slot (`stage_slots.js`
|
|
428
|
+
`drawAsking(text, refs)`) pairs them on its raw draw and again after its markdown swap; the card pairs its
|
|
429
|
+
question text and its kept reply (`question_cards.js`); the step's closed `.gen-text` takes them
|
|
430
|
+
(`turn_view.js`; on a reload its `data-answer-at`, the message's `display_refs`) and `linkTextRefs` pairs it at
|
|
431
|
+
the turn's end and after a reload. A file ref outside the
|
|
425
432
|
session's folder is dropped, as a reload drops it: the folder `/tail` names, else (an older worker) the hub's
|
|
426
433
|
session list entry's, else (refs to check, no hub entry) the session file's; with no folder known at all, every
|
|
427
434
|
file ref is dropped rather than shown unchecked.
|
|
@@ -507,7 +514,8 @@ Bridge's stream above): the chat loop's thinking is the
|
|
|
507
514
|
server's `reasoning_content`; the raw-prompt loop splits each generation's
|
|
508
515
|
stream with a fresh `ThoughtStreamSplitter`, seeded with the prompt's prefill (Qwen's `<think>` and Gemma's
|
|
509
516
|
`<|channel>thought … <channel|>` are thinking, tool-call bodies are dropped
|
|
510
|
-
from `text
|
|
517
|
+
from `text:`; a saved step's text, as the web history and the attached terminal's join show it,
|
|
518
|
+
drops both too: `OutputFormatter.strip_markup`). The chat loop's model turns keep their `tool_calls` and tool results their `tool_call_id`,
|
|
511
519
|
so later requests and resumed sessions pair them, and their reasoning (`thinking:`), which goes back as
|
|
512
520
|
`reasoning_content` on the running turn's replies (`reasoning_history`; a step-limit Continue's
|
|
513
521
|
turn goes on from the run it resumes, `reasoning_since:`). It has its own system prompt
|
|
@@ -604,7 +612,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
604
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'. |
|
|
605
613
|
| Images / vision | `image_store.rb` (`ImageStore`), `vision_support.rb`, `vision_context.rb` | Stored prompt images and whether a model takes them. |
|
|
606
614
|
| 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)). |
|
|
607
|
-
| 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)). |
|
|
615
|
+
| 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)). |
|
|
608
616
|
| 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)). |
|
|
609
617
|
| 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)). |
|
|
610
618
|
| 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
|
@@ -51,7 +51,8 @@ plugin:
|
|
|
51
51
|
sha256: sha256:<hex> # `chi bundle check --fix` fills it; never compute it by hand
|
|
52
52
|
# files: # memories it installs (top-level *.md); without files: every *.md but README & co.
|
|
53
53
|
# my_notes.md: sha256:<hex>
|
|
54
|
-
# hooks: # hooks/*.rb files (a plugin's chi.on is usually simpler)
|
|
54
|
+
# hooks: # hooks/*.rb files (a plugin's chi.on is usually simpler), each with event: NAME
|
|
55
|
+
# # or events: [a, b] (one instance for both; chi newer than 0.97.0)
|
|
55
56
|
# needs: [gh] # outside commands it relies on
|
|
56
57
|
```
|
|
57
58
|
|
|
@@ -64,7 +65,7 @@ time. `chi bundle check --set KEY=VALUE` gives the sandboxed load a setting.
|
|
|
64
65
|
## What a hook sees
|
|
65
66
|
|
|
66
67
|
`chi.on(event) { |event, ctx| … }` gets a Hash. The events, in a turn's order: `session_start`, `before_turn`,
|
|
67
|
-
`before_generation`, `generation_progress`, `after_generation`, `before_tool_call`, `after_tool_call`,
|
|
68
|
+
`before_generation`, `generation_progress`, `after_generation`, `before_tool_call`, `question_asked`, `after_tool_call`,
|
|
68
69
|
`after_turn`, `session_end` (the fields of each: [hooks.md, Hook Events](hooks.md#hook-events)). A symbol not in
|
|
69
70
|
that list is never fired (`check` warns). The two a bundle wants most, for an `execute`:
|
|
70
71
|
|
|
@@ -116,7 +117,8 @@ end
|
|
|
116
117
|
|
|
117
118
|
`bundle.tool_call` runs guarded hooks; `bundle.tool` calls a registered tool. `bundle.steers`, `cards`, `stops` and
|
|
118
119
|
`questions` record effects; `bundle.answer` / `model_answers` provide `ask_user` / `ask_model` answers.
|
|
119
|
-
`bundle.http.stub` stubs `ctx.http`; `
|
|
120
|
+
`bundle.http.stub` stubs `ctx.http`; `bundle.question_asked(text, question:)` fires `:question_asked` and returns
|
|
121
|
+
the refs; `sandbox.config`, `sandbox.fake_executable` and `bundle.install` cover setup.
|
|
120
122
|
The full list is in [plugins.md, Testing a bundle](plugins.md#testing-a-bundle-experimental). The kit is
|
|
121
123
|
experimental (it may change before chi 1.0). If a test needs a fake of chi's own (a stub of a `Samagotchi::`
|
|
122
124
|
class, a hand-built `ctx`), that is a gap in the kit: say so to the user rather than reaching into chi.
|
data/docs/cli.md
CHANGED
|
@@ -659,8 +659,8 @@ the first message: …)`); `/title TEXT` names the session (whitespace collapsed
|
|
|
659
659
|
cut at 80 characters), `/title --clear` drops the name (anything after `--clear`
|
|
660
660
|
is refused with the usage line). It is saved at once,
|
|
661
661
|
mid-turn too, and the lists, cards, top bar and tab show it in place of the first
|
|
662
|
-
message; a continue's next link keeps it.
|
|
663
|
-
[Session titles](sessions.md#session-titles).
|
|
662
|
+
message; a continue's next link keeps it. In the web, `✎` next to the session's
|
|
663
|
+
title (its strip card, or the top bar) does it. See [Session titles](sessions.md#session-titles).
|
|
664
664
|
|
|
665
665
|
A `/thinking`, `/reasoning-history` or `/toolkit` change that restarts the prompt cache
|
|
666
666
|
asks first: the reply says what it costs, changes nothing, and names the line
|
|
@@ -895,7 +895,12 @@ nudged the model") show ahead of those calls for about 4 s, as their rows
|
|
|
895
895
|
wait in the closed block. A card that needs you (a question, an approval, check-in)
|
|
896
896
|
sits in the stage too. While a question or approval waits, the stage shows
|
|
897
897
|
the whole text of the reply that asked it (in place of its newest
|
|
898
|
-
sentence) above the card, until you answer.
|
|
898
|
+
sentence) above the card, until you answer. A long reply that asks the
|
|
899
|
+
model's own question (one that doesn't fit that slot) gets more room: the
|
|
900
|
+
stage grows to about 85% of the page for the rest of the turn (never above
|
|
901
|
+
the session panel), the reply reads from its start in the stage's one
|
|
902
|
+
scroll, and while the card is out of view a `❓ Answer ↓` pill at the
|
|
903
|
+
stage's bottom edge takes you to it. Once answered, the model's
|
|
899
904
|
question or its own approval keeps that reply (a delegate's relayed
|
|
900
905
|
approval, a hook's question and the step limit don't): two muted lines
|
|
901
906
|
under its `question → answer` line, the whole text when you open the card,
|
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
|
@@ -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
|
@@ -56,6 +56,7 @@ The plugin class must respond to `#call(event)` — duck-typed, no base class re
|
|
|
56
56
|
| `:after_generation` | After LLM returns (both loops); not after a generation a hook cut (`stop_generation`) | `{ type: :after_generation, iteration: N, response: "...", messages: [...] (the conversation as sent) }` |
|
|
57
57
|
| `:generation_progress` | While the response streams, in batches (see [Watching the stream](#watching-the-stream)) | `{ type: :generation_progress, iteration: N, thinking: "..." (new since the last fire), text: "..." (new visible text), thinking_chars: N, text_chars: N (this generation so far), elapsed_ms: N, restarted: true (only on the first fire after a dropped stream was asked again) }` |
|
|
58
58
|
| `:before_tool_call` | Before tool dispatch (and before `tool_call_started`) | `{ type: :before_tool_call, iteration: N, call: {...}, params: "...", guardrail: Verdict, context: {...}, targets: {...}, blocked: false, block_reason: nil }` |
|
|
59
|
+
| `:question_asked` | A question of the model's own (`ask_user_question`) or an approval of its tool call is about to open, on the turn's thread (not a hook's or plugin's `ask_user`, a relayed delegate approval, the step-limit question, a `--non-interactive` run or a stored approval: none of those fire it) | `{ type: :question_asked, kind: "question" \| "approval", text: "..." (the reply that asks, as the asking slot shows it: no thoughts, no tool-call markup), question: {...} (the question's fields, frozen: question/options/header…, an approval's approval: facts too; no id yet), refs: (see [Refs on a question](#refs-on-a-question-display-only)) }` |
|
|
59
60
|
| `:after_tool_call` | After tool execution | `{ type: :after_tool_call, iteration: N, tool: "read", output: "..." (the text the model gets: capped at max_tool_output_chars; a cut one has a [cut: …] line, at its end, or for an execute result between its head and its tail, the whole output saved in the session's tool_output/ folder), status: "ok" \| "error" \| "blocked" \| "stopped", call: {...} (the call that ran, as the gate left it), elapsed_ms: N (the tool's own run, without an approval wait; nil for a denied call), result: "..." (the tool's own text: no "[read]" lead, no "ran as:" line, never cut) }`; `stopped` is a `task_wait` the turn's Stop ended (the task runs on) or whose task was stopped. Match on `result:`, not `output:`: it is what the tool said (`Error: command timed out after 120s` for an execute killed at its limit in kill mode, `Samagotchi::Tools::Execute::TIMEOUT_PREFIX`; `Still running at the 120s limit: …` for one handed over as a task, `Samagotchi::Tools::Execute::HANDED_OVER`) |
|
|
60
61
|
| `:session_end` | After each turn `:after_turn` fires for, after it (turn-level lifecycle) | `{ type: :session_end, session_id: "..." }` |
|
|
61
62
|
|
|
@@ -366,6 +367,58 @@ end
|
|
|
366
367
|
|
|
367
368
|
A plugin gets the same from `chi.on(:after_turn) { |event, ctx| event[:refs].call(…) }`.
|
|
368
369
|
|
|
370
|
+
## Refs on a question (display only)
|
|
371
|
+
|
|
372
|
+
`:question_asked` carries `refs:` too, for the reply that asks the question: a
|
|
373
|
+
ticket the model names while it asks is a ref while the question waits, when
|
|
374
|
+
the user needs it.
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
class JiraQuestionRefs
|
|
378
|
+
def call(event)
|
|
379
|
+
return unless event[:type] == :question_asked
|
|
380
|
+
|
|
381
|
+
event[:text].scan(/\b[A-Z][A-Z0-9]+-\d+\b/).uniq.each do |key|
|
|
382
|
+
event[:refs].call(match: key, kind: "issue", target: { id: key }, label: key,
|
|
383
|
+
actions: [{ id: "open", label: "Open in Jira", url: "https://jira.example.com/browse/#{key}" }])
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
- The call, its return values, the checks and the `refs_rejected` log are
|
|
390
|
+
[Refs on the answer](#refs-on-the-answer-display-only)'s, with lower caps:
|
|
391
|
+
50 refs and 32 KiB per step. `nil`: the turn has no model step to attach to,
|
|
392
|
+
or the call came after the hooks ran.
|
|
393
|
+
- They are kept as `display_refs` on the step's model message (the one that
|
|
394
|
+
asks; in the session file after the turn, a cancelled one too) and go on the
|
|
395
|
+
pending question as its `refs` (the `question_requested` event, the session
|
|
396
|
+
file while it waits, the saved question card). A second question in the same
|
|
397
|
+
step adds to the step's list, and its pending question has the whole list.
|
|
398
|
+
The model never sees them.
|
|
399
|
+
- The web checks them again wherever a saved one reaches it (the pending
|
|
400
|
+
question, a saved card, the history), as an answer's, and pairs them as an
|
|
401
|
+
answer's (the three passes above) in: the stage's asking slot while the
|
|
402
|
+
question waits (its raw text, then again once its markdown is in), the
|
|
403
|
+
card's question text, the answered card's kept reply (raw, then its
|
|
404
|
+
markdown when opened; never its one-line summary copy), and that step's
|
|
405
|
+
text in the history, live (at the turn's end) and after a reload. A ref
|
|
406
|
+
whose `match` is only in the question's text pairs there alone; the step's
|
|
407
|
+
text drops it. **Terminals** ignore them.
|
|
408
|
+
- **Its time is the question's wait**: the hooks run before the question
|
|
409
|
+
opens, so a slow one delays it (and counts in `ask_user_question`'s
|
|
410
|
+
`elapsed_ms` and an approval's `waited_ms`). A hook that raises is logged and
|
|
411
|
+
the question opens with the other hooks' refs. With no hook on the event
|
|
412
|
+
nothing is built.
|
|
413
|
+
- Don't ask from it: `event[:ask_user]` (and `ctx.ask_user` in a `chi.on`
|
|
414
|
+
block) returns nil at once here.
|
|
415
|
+
- Needs chi newer than 0.97.0: set `requires_chi`.
|
|
416
|
+
|
|
417
|
+
A plugin gets the same from `chi.on(:question_asked) { |event, ctx| event[:refs].call(…) }`. The bundle test
|
|
418
|
+
kit fires it as chi does: `bundle.question_asked(text, kind:, question:)` returns the refs the step gets
|
|
419
|
+
([plugins.md](plugins.md#testing-a-bundle-experimental)). The shipped source-links bundle subscribes (its
|
|
420
|
+
sources' refs in the reply that asks, and in the question's text and options).
|
|
421
|
+
|
|
369
422
|
## Settings
|
|
370
423
|
|
|
371
424
|
A hook class whose `initialize` takes an argument gets its settings: **one
|
|
@@ -557,6 +610,11 @@ A built-in reached through `call_tool` (`chi/delegate`, a tool the session's too
|
|
|
557
610
|
declare) fires as that built-in, its call built as a direct call's, with `call[:via] == "call_tool"`.
|
|
558
611
|
A hook that drops `via:` or rewrites the call to a tool the toolkit removes gets the call refused
|
|
559
612
|
by its name.
|
|
613
|
+
On a Messages host (`api: anthropic`), the model may call those tools by their own names (they
|
|
614
|
+
go out deferred, [configuration.md](configuration.md#toolkits)): a direct `delegate` fires as a
|
|
615
|
+
declared tool's call, with no `via:`; a direct `mcp_fs_read` fires as the `call_tool` call it
|
|
616
|
+
stands for (`tool: "call_tool"`, `args: {"tool" => "fs/read", "args" => {…}}`), as if the model
|
|
617
|
+
had called `call_tool`.
|
|
560
618
|
|
|
561
619
|
## Bundle Hooks (unified workflow bundle)
|
|
562
620
|
|
|
@@ -599,6 +657,12 @@ Notes:
|
|
|
599
657
|
|
|
600
658
|
- Hook key = basename (flat under `hooks/`). No subdirs in v1.
|
|
601
659
|
- `event` is required for auto-registration; a hook with no event is skipped.
|
|
660
|
+
- `events: [after_turn, question_asked]` (a list, in place of `event:`) registers the hook for each event it
|
|
661
|
+
names: **one** instance serves them all (its state is shared), and `#call` tells them apart by
|
|
662
|
+
`event[:type]`. `event:` and `events:` together, or an `events:` that isn't a non-empty list of event names,
|
|
663
|
+
is refused at install (`hooks: <file>: …`). `on_error: fail_closed` applies to its `before_tool_call`
|
|
664
|
+
registration only. Needs chi newer than 0.97.0 (an older chi installs the hook with no event and never loads
|
|
665
|
+
it): set `requires_chi`.
|
|
602
666
|
- `sha256` is integrity (not authenticity). No signing in v1. Install records the sha256 of the copied file; at `Engine.new` a hook whose file differs is not loaded (reinstall the bundle after editing one by hand).
|
|
603
667
|
- Hook code is the bundle author's source of truth: on upgrade, hooks are overwritten; if the installed file was locally modified, a warning is emitted (`was locally modified; overwriting`).
|
|
604
668
|
- `on_error` defaults to `skip` for every event, `:before_tool_call` included: a raising hook is silent and the call goes ahead. `log` warns. `fail_closed` (only on `:before_tool_call`; on any other event it acts as `skip`) denies the call the hook raised on. Choose `fail_closed` for a hook that is a guardrail, whose failure should stop tool calls rather than let them through.
|
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/bundles.md
CHANGED
|
@@ -37,7 +37,7 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
37
37
|
| `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps) |
|
|
38
38
|
| `Merger` | 3-way classification per file: `install`, `fast_forward`, `keep`, `noop`, `conflict` |
|
|
39
39
|
| `Provenance` | the `.bundles/<name>/` dir: `#write` the record + bases, `#record` → `InstalledBundle`, `.each_installed`, `.claimants` (which bundles own a file) |
|
|
40
|
-
| `InstalledBundle`, `BundleHook`, `PluginRef` | value objects for the record; tolerant of older records (missing keys read as empty) |
|
|
40
|
+
| `InstalledBundle`, `BundleHook`, `PluginRef` | value objects for the record; tolerant of older records (missing keys read as empty). `BundleHook#events` is the manifest's `event:` or its `events:` list (written back as `event:` for one, as an older chi reads it); `Hooks::BundleLoader#load_hook_file` builds the hook once and registers it under each |
|
|
41
41
|
| `Builder` | `chi bundle build`: memories (+ the installed bundle's hooks, rules, plugin, scripts) → zip/tar/dir |
|
|
42
42
|
| `Profile` | meta bundles (`includes:`): install the members, record which; `InstallResult#kept_own` names a new member's memory files the user already had, with other bytes (`kept_own_line`, printed by `chi bundle`, `chi update` and `chi bootstrap`); a dry run runs each member's Installer as a dry run too, so it names them as well |
|
|
43
43
|
| `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` |
|
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)
|
|
@@ -594,6 +604,23 @@ logged). Both are display only: `AnswerDisplay::DISPLAY_ONLY` lists `display`, `
|
|
|
594
604
|
after_generation), while `KernelLoop#sanitize_history` and `ChatLoop#plain` copy them so the stored conversation keeps
|
|
595
605
|
them across turns. `Web::App#messages_for_display` sends `display_refs` re-validated (`AnswerRefs.for_display`).
|
|
596
606
|
|
|
607
|
+
`:question_asked` (`QuestionAsked`, `question_asked.rb`) fires from the two Engine seams where a question of the
|
|
608
|
+
model's opens, never from `QuestionDesk`: `Engine#request_question` (the kernel's `question_handler`, after
|
|
609
|
+
`AskUserQuestion.validate`; not with `interface == :non_interactive`) and the guardrail wiring's `ask:` lambda
|
|
610
|
+
(`GuardrailWiring#request_approval`, after a stored approval and the non-interactive deny). So `ask_for_hook`, a
|
|
611
|
+
relayed approval (`EnginePeers#open_question` → `Engine#open_question`) and `post` (the step-limit question) never
|
|
612
|
+
fire it. `QuestionAsked.fire` is a no-op without a subscriber (`Registry#any?`); else it finds the live
|
|
613
|
+
conversation's last `role: "model"` entry (`Engine#live_conversation`, one lookup for both loops: a ChatLoop batch may
|
|
614
|
+
end in a tool result), builds an `AnswerRefs` on that step alone with the question caps (50 / 32 KiB), fires with the
|
|
615
|
+
step's text (`Engine#question_step_text`: the parser's thought and tool-call strip, then
|
|
616
|
+
`OutputFormatter.strip_markup`), a frozen copy of the fields and a no-op `ask_user`, then sets `display_refs` on
|
|
617
|
+
the step **in place** under the event lock (the loops and the forget layer hold entries by identity) and returns the
|
|
618
|
+
fields with `refs:`. `QuestionDesk#request` forwards `refs` past its slice; `PendingQuestion#refs` carries them to
|
|
619
|
+
`:question_requested`, the session file and the cards. `Web::App#with_question_refs` re-validates them (the pending
|
|
620
|
+
question, `cards_for_display`) with the same caps; the page pairs them in the asking slot, on the card and on the
|
|
621
|
+
step's text ([architecture.md](../architecture.md), the `display_refs` paragraph). The kit's
|
|
622
|
+
`Testing::Bundle#question_asked` calls `QuestionAsked.fire` with a one-step conversation.
|
|
623
|
+
|
|
597
624
|
### Hook lifecycle
|
|
598
625
|
|
|
599
626
|
- **Bundle hooks** (`Registry#register_bundle`) and **config hooks** (`register_persistent`, from config.yml)
|
|
@@ -756,6 +783,7 @@ Paths are under `lib/samagotchi/`.
|
|
|
756
783
|
| `toolkit_binding.rb` | `ToolkitBinding`: the Engine's toolkit seam (the kits, `#explained`, `ctx.use_toolkit`, the turn-start `#apply!` behind `Engine#apply_toolkit!`) |
|
|
757
784
|
| `hooks/registry.rb` | Hook registry, fire order, `Hooks::Runtime` |
|
|
758
785
|
| `hook_routes.rb` | `HookRoutes`: the Engine's routes for a hook's or plugin's notice (`:hook_notice`, `fallback_for:`), question, stop and steer, and the guardrails' after-load line |
|
|
786
|
+
| `question_asked.rb` | `QuestionAsked`: the `:question_asked` fire, the asking step's refs (`display_refs` in place, the pending question's `refs`) |
|
|
759
787
|
| `question_desk.rb`, `pending_question.rb`, `question_answer.rb` | `QuestionDesk`: the one open question (ask, post, answer, cancel, relay mark); `PendingQuestion` and `QuestionAnswer`, its values (Hashes only in events, Bridge JSON, the session file and the hook/plugin `ask_user` result) |
|
|
760
788
|
| `hook_question.rb` | `HookQuestion`: a hook's or plugin's `ask_user` question as one value, from the runtime/host edge to `QuestionDesk#ask_for_hook` |
|
|
761
789
|
| `event_routes.rb` | `EventRoutes`: where a card (`Engine#show_card`), a notice (`HookRoutes#notify`) and a guardrails line (`#guardrail_notify`, turn or between turns only) go: held while the plugins load, an anytime command's, an init task's, a running turn's event, else announced or held for `holding_announcements` |
|
|
@@ -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
|
|