samagotchi 0.66.0 → 0.68.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 +80 -1
- data/docs/architecture.md +55 -30
- data/docs/cli.md +3 -1
- data/docs/configuration.md +50 -9
- data/docs/context.md +35 -1
- data/docs/desktop.md +90 -22
- data/docs/guardrails.md +8 -5
- data/docs/hooks.md +2 -2
- data/docs/internals/anthropic.md +156 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +2 -0
- data/docs/internals/engine.md +129 -38
- data/docs/internals/gemma4-contract.md +1 -1
- data/docs/internals/llm-context-bench.md +5 -4
- data/docs/internals/prompt-caching.md +26 -11
- data/docs/plugins.md +126 -23
- data/docs/sessions.md +6 -6
- data/docs/sub-agent.md +4 -1
- data/docs/testing.md +18 -4
- data/lib/samagotchi/bootstrap/probe.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +13 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
- data/lib/samagotchi/bridge.rb +6 -3
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +34 -313
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +14 -6
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +37 -312
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +14 -6
- data/lib/samagotchi/bundles/mcp/manifest.yml +5 -5
- data/lib/samagotchi/bundles/mcp/plugin.rb +82 -294
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +15 -7
- data/lib/samagotchi/context_absorber.rb +66 -13
- data/lib/samagotchi/desktop/macos/AgentTerminal.swift +260 -0
- data/lib/samagotchi/desktop/macos/App.swift +3 -3
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +5 -2
- data/lib/samagotchi/desktop/macos/Images.swift +2 -2
- data/lib/samagotchi/desktop/macos/Kitty.swift +33 -197
- data/lib/samagotchi/desktop/macos/Panel.swift +118 -82
- data/lib/samagotchi/desktop/macos/ProcessRunner.swift +1 -1
- data/lib/samagotchi/desktop/macos/Tmux.swift +239 -0
- data/lib/samagotchi/desktop/macos.rb +84 -13
- data/lib/samagotchi/desktop_command.rb +14 -3
- data/lib/samagotchi/effective_model.rb +283 -0
- data/lib/samagotchi/engine.rb +307 -549
- data/lib/samagotchi/engine_peers.rb +3 -0
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +1 -1
- data/lib/samagotchi/guardrails/approvals.rb +1 -1
- data/lib/samagotchi/guardrails/rules.rb +1 -1
- data/lib/samagotchi/guardrails/targets.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +151 -0
- data/lib/samagotchi/hooks/registry.rb +2 -2
- data/lib/samagotchi/host_keys.rb +147 -0
- data/lib/samagotchi/level_setting_command.rb +99 -0
- data/lib/samagotchi/llm/adapter_common.rb +138 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +28 -105
- data/lib/samagotchi/llm/api_key.rb +23 -7
- data/lib/samagotchi/llm/chat_loop.rb +10 -6
- data/lib/samagotchi/llm/openai_chat.rb +9 -92
- data/lib/samagotchi/llm/refusal_memo.rb +29 -0
- data/lib/samagotchi/llm_context_override.rb +1 -0
- data/lib/samagotchi/llm_context_strategy.rb +5 -10
- data/lib/samagotchi/peer_sources.rb +58 -0
- data/lib/samagotchi/plugin/api.rb +64 -155
- data/lib/samagotchi/plugin/loader.rb +4 -2
- data/lib/samagotchi/plugin/peer.rb +394 -0
- data/lib/samagotchi/plugin/tool_spec.rb +166 -0
- data/lib/samagotchi/plugin_tasks.rb +2 -2
- data/lib/samagotchi/reasoning_history_command.rb +29 -90
- data/lib/samagotchi/row_refs.rb +1 -1
- data/lib/samagotchi/self_report.rb +5 -3
- data/lib/samagotchi/send_command.rb +21 -11
- data/lib/samagotchi/session.rb +22 -8
- data/lib/samagotchi/session_continue.rb +58 -15
- data/lib/samagotchi/session_inbox.rb +0 -13
- data/lib/samagotchi/session_setting.rb +86 -0
- data/lib/samagotchi/thinking_command.rb +24 -76
- data/lib/samagotchi/thinking_notices.rb +169 -0
- data/lib/samagotchi/tool_call_parser.rb +1 -1
- data/lib/samagotchi/toolkit_binding.rb +138 -0
- data/lib/samagotchi/tools/context_read.rb +14 -0
- data/lib/samagotchi/tools/execute.rb +12 -12
- data/lib/samagotchi/tools/registry.rb +1 -1
- data/lib/samagotchi/tools/task_create.rb +6 -6
- data/lib/samagotchi/tools/task_get.rb +13 -13
- data/lib/samagotchi/tools/task_list.rb +6 -6
- data/lib/samagotchi/tools/task_record.rb +52 -0
- data/lib/samagotchi/tools/task_runtime.rb +55 -91
- data/lib/samagotchi/tools/task_stop.rb +6 -6
- data/lib/samagotchi/tools/task_wait.rb +12 -13
- data/lib/samagotchi/tools/tool_index.rb +454 -0
- data/lib/samagotchi/update_command.rb +9 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +71 -93
- data/lib/samagotchi/web/capabilities.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +2 -2
- data/lib/samagotchi/web/public/annotate_actions.js +1 -1
- data/lib/samagotchi/web/public/app.js +103 -539
- data/lib/samagotchi/web/public/bubbles.js +291 -0
- data/lib/samagotchi/web/public/cards_place.js +184 -0
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/chip_popover.js +111 -0
- data/lib/samagotchi/web/public/command_list.js +12 -10
- data/lib/samagotchi/web/public/commands.js +2 -2
- data/lib/samagotchi/web/public/composer_size.js +2 -2
- data/lib/samagotchi/web/public/copy.js +1 -1
- data/lib/samagotchi/web/public/ctx.js +1 -1
- data/lib/samagotchi/web/public/delivery_line.js +4 -4
- data/lib/samagotchi/web/public/families_view.js +1 -1
- data/lib/samagotchi/web/public/format.js +11 -7
- data/lib/samagotchi/web/public/images.js +25 -2
- data/lib/samagotchi/web/public/index.html +15 -7
- data/lib/samagotchi/web/public/info_bar.js +3 -3
- data/lib/samagotchi/web/public/info_bar_model.js +110 -0
- data/lib/samagotchi/web/public/init_row.js +21 -10
- data/lib/samagotchi/web/public/level_chip.js +29 -80
- data/lib/samagotchi/web/public/lists_view.js +64 -9
- data/lib/samagotchi/web/public/llm_context_chip.js +38 -107
- data/lib/samagotchi/web/public/model_picker.js +26 -22
- data/lib/samagotchi/web/public/notify.js +6 -4
- data/lib/samagotchi/web/public/question_card.js +1 -1
- data/lib/samagotchi/web/public/ref_scan.js +2 -8
- data/lib/samagotchi/web/public/refs.js +13 -0
- data/lib/samagotchi/web/public/session_state.js +14 -0
- data/lib/samagotchi/web/public/session_title.js +2 -3
- data/lib/samagotchi/web/public/sessions_list.js +0 -8
- data/lib/samagotchi/web/public/stage_frame.js +179 -0
- data/lib/samagotchi/web/public/stage_model.js +35 -2
- data/lib/samagotchi/web/public/stage_slots.js +130 -0
- data/lib/samagotchi/web/public/stage_view.js +56 -335
- data/lib/samagotchi/web/public/stage_watch.js +65 -0
- data/lib/samagotchi/web/public/strip.js +3 -3
- data/lib/samagotchi/web/public/task_stop.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +2 -2
- data/lib/samagotchi/web/public/turn_model.js +2 -2
- data/lib/samagotchi/web/public/turn_view.js +1 -1
- data/lib/samagotchi/web/public/vendor/directive-helpers.js +8 -0
- data/lib/samagotchi/web/public/vendor/directives/repeat.js +9 -0
- data/lib/samagotchi/web/server.rb +0 -4
- data/lib/samagotchi/worker.rb +22 -24
- data/lib/samagotchi/worker_inbound.rb +36 -4
- data/lib/samagotchi/worker_wakes.rb +67 -7
- metadata +27 -1
data/docs/desktop.md
CHANGED
|
@@ -20,7 +20,8 @@ sessions. Keys:
|
|
|
20
20
|
- **⏎** sends a message: `chi send -m <the line>` with the text as quoted context above it. With the line empty,
|
|
21
21
|
the text is the message.
|
|
22
22
|
- **⌘⏎** sends a note: `chi note`, the line (if any) then the text.
|
|
23
|
-
- **⌥⏎** pastes into an agent in a kitty window without pressing Enter (see
|
|
23
|
+
- **⌥⏎** pastes into an agent in a kitty window or tmux pane without pressing Enter (see
|
|
24
|
+
[Agents in kitty and tmux](#agents-in-kitty-and-tmux));
|
|
24
25
|
it is not a newline.
|
|
25
26
|
- **⇧⏎** is a newline, in either field. Esc closes.
|
|
26
27
|
|
|
@@ -105,11 +106,15 @@ thumbnails sit under the message line, 48 px high, the file name as a tooltip; h
|
|
|
105
106
|
the panel closes; files from Finder are sent as they are, never touched. A send with images may take up to 30 s
|
|
106
107
|
(converting, a worker starting) before the panel gives up.
|
|
107
108
|
|
|
108
|
-
## Agents in kitty
|
|
109
|
+
## Agents in kitty and tmux
|
|
109
110
|
|
|
110
111
|
The panel can also send to agent CLIs (claude, codex, gemini, aider, …) running in
|
|
111
|
-
[kitty](https://sw.kovidgoyal.net/kitty/) windows
|
|
112
|
-
off until you set it up
|
|
112
|
+
[kitty](https://sw.kovidgoyal.net/kitty/) windows or [tmux](https://github.com/tmux/tmux) panes, next to chi
|
|
113
|
+
sessions. Each is off until you set it up; both can be on.
|
|
114
|
+
|
|
115
|
+
### kitty
|
|
116
|
+
|
|
117
|
+
Through kitty's remote control:
|
|
113
118
|
|
|
114
119
|
1. In `kitty.conf`, remote control on and a socket: `allow_remote_control yes` and a `listen_on unix:…` line.
|
|
115
120
|
2. Copy that `listen_on` value as it is into chi's config.yml:
|
|
@@ -121,7 +126,7 @@ off until you set it up:
|
|
|
121
126
|
|
|
122
127
|
`kitty.agents` picks the programs listed (default `claude|codex|gemini|aider|opencode|cursor-agent|amp|goose`, a
|
|
123
128
|
YAML list works too; `"*"` lists every window, shells too), `kitty.binary` the kitty to run (default
|
|
124
|
-
`/Applications/kitty.app/Contents/MacOS/kitty`). See [Configuration](configuration.md#desktop-helper-agents-in-kitty).
|
|
129
|
+
`/Applications/kitty.app/Contents/MacOS/kitty`). See [Configuration](configuration.md#desktop-helper-agents-in-kitty-and-tmux).
|
|
125
130
|
3. Run `chi update` (or `chi desktop upgrade`): the helper reads these from its launch file, so **after every edit of
|
|
126
131
|
`kitty:`** run it again. `chi update` only rewrites the launch file when only the settings changed (no rebuild);
|
|
127
132
|
`chi desktop upgrade` always rebuilds.
|
|
@@ -130,28 +135,66 @@ The helper finds the sockets itself: kitty adds `-<pid>` to a `listen_on` path f
|
|
|
130
135
|
`{kitty_pid}`), `~` and variables the helper knows are expanded, others (`${KITTY_PID}`) stay as written, as kitty
|
|
131
136
|
keeps them. Several kitty instances all count. Only `unix:` sockets work.
|
|
132
137
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
A window counts when a foreground process's program is an agent, also as the script of `node …/codex` (that match is
|
|
139
|
+
untested for codex and gemini).
|
|
140
|
+
|
|
141
|
+
### tmux
|
|
142
|
+
|
|
143
|
+
Any terminal works (iTerm, Terminal, Ghostty, kitty…) for agents started inside tmux, with no macOS permission to
|
|
144
|
+
grant:
|
|
145
|
+
|
|
146
|
+
1. In chi's config.yml:
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
tmux:
|
|
150
|
+
enabled: true
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`tmux.agents` picks the programs listed (kitty.agents' default; `"*"` lists every pane), `tmux.binary` the tmux
|
|
154
|
+
to run (default: the `tmux` on your shell's PATH when the launch file is written, e.g. `/opt/homebrew/bin/tmux`;
|
|
155
|
+
the helper itself has no PATH). See [Configuration](configuration.md#desktop-helper-agents-in-kitty-and-tmux).
|
|
156
|
+
2. Run `chi update` (or `chi desktop upgrade`), and again after every edit of `tmux:`. With no tmux found they say
|
|
157
|
+
"tmux.enabled but no tmux found on PATH: set tmux.binary", and the panel "tmux not found…".
|
|
137
158
|
|
|
138
|
-
|
|
159
|
+
Every tmux server whose socket is in `$TMUX_TMPDIR/tmux-<uid>` (`/tmp/tmux-<uid>` by default) counts: the default
|
|
160
|
+
server and `tmux -L <name>` ones. A server started with `-S` elsewhere, or a tmux on another machine (inside `ssh`),
|
|
161
|
+
isn't seen. The socket dir and the binary are read when the launch file is written, like the rest of it.
|
|
162
|
+
|
|
163
|
+
A pane counts when its foreground program is an agent. The helper asks `ps`, not tmux's `pane_current_command`: that
|
|
164
|
+
names the native claude by its version file ("2.1.296"). A pane's title is shown unless it is tmux's default (the
|
|
165
|
+
host name).
|
|
166
|
+
|
|
167
|
+
### In the panel
|
|
168
|
+
|
|
169
|
+
The panel lists the agents between live and recent sessions: "claude · <its title>", the folder below, a terminal
|
|
170
|
+
icon, and a ⌘ number like the sessions. With one terminal on, the divider says **kitty** (or **tmux**); with both,
|
|
171
|
+
it says **agents**, kitty's rows come first and each row has a small `kitty` or `tmux` tag. With the settings on but
|
|
172
|
+
no agent running, one dim line says "no agents in kitty" ("in tmux", "in kitty or tmux"); a terminal that can't be
|
|
173
|
+
asked gets a dim line with why, also beside the other terminal's rows. The list shows at most 9 targets: many agents
|
|
174
|
+
can push recent sessions out.
|
|
175
|
+
|
|
176
|
+
- **⏎** pastes into every chosen agent and presses Enter: the text as a `> ` quote, a blank line, your message, then
|
|
139
177
|
one image path per line. Chosen together with chi sessions, each gets the message and the panel shows one line
|
|
140
178
|
("Sent. · sent to claude · samagotchi").
|
|
141
|
-
- **⌥⏎** only pastes (no Enter), and only when every chosen target is
|
|
142
|
-
each with its own line, then press Enter in
|
|
143
|
-
- **⌘⏎** with
|
|
144
|
-
for kitty
|
|
179
|
+
- **⌥⏎** only pastes (no Enter), and only when every chosen target is an agent: build up several screenshots,
|
|
180
|
+
each with its own line, then press Enter in the terminal. The footer shows **Paste ⌥⏎** instead of Note then.
|
|
181
|
+
- **⌘⏎** with an agent chosen beeps: "Notes are for chi sessions". ⌥⏎ with a chi session: "Paste only is for
|
|
182
|
+
kitty windows" ("… for tmux panes", "… for agents in kitty or tmux" when tmux is on).
|
|
145
183
|
- **Images:** a screenshot or other image data is copied to `$TMPDIR/chi-helper-sent/<uuid>.png` (or `.tiff`) and
|
|
146
184
|
its path pasted; the agent reads it from there (claude attaches it as `[Image #1]`). These files are deleted a week
|
|
147
185
|
later, at the helper's next start. Finder files are pasted with their own path, `'…'`-quoted when they hold spaces.
|
|
148
186
|
- The paste arrives as one bracketed paste, so the agent takes newlines as text (claude shows long ones as
|
|
149
|
-
`[Pasted text]`).
|
|
150
|
-
act as Enter.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
187
|
+
`[Pasted text]`). In kitty, with `agents: "*"`, beware of a program that doesn't turn bracketed paste on: the
|
|
188
|
+
newlines would act as Enter. tmux brackets the paste only for a program that asked for it, and keeps the newlines
|
|
189
|
+
as typed, so they don't act as Enter there.
|
|
190
|
+
- tmux: a pane scrolled back (copy mode) is taken out of it first, so the paste and Enter reach the agent (the
|
|
191
|
+
scroll position is lost). A pane in another tmux mode (the `choose-tree` tree, the clock) is refused: "pane in
|
|
192
|
+
tree-mode".
|
|
193
|
+
- No delivery confirmation: before pasting, the helper checks the window or pane is still there ("window closed",
|
|
194
|
+
"pane closed" if not; also for a pane of a tmux server restarted since the panel listed it); it can't know whether
|
|
195
|
+
the agent read it. The 16 KB cap is chi's; a terminal takes more.
|
|
196
|
+
- Focus goes back to the app you came from, as for sessions. The last choice (agents too) is preselected while
|
|
197
|
+
listed; ⌘ numbers shift as agents come and go.
|
|
155
198
|
|
|
156
199
|
## Install
|
|
157
200
|
|
|
@@ -182,7 +225,7 @@ rewrites `launch.json`, which the app reads at each send, so the app keeps its o
|
|
|
182
225
|
| `chi desktop install [--force] [--login]` | builds, installs and starts it; `--force` replaces an existing copy |
|
|
183
226
|
| `chi desktop upgrade` | rebuilds it for this chi and restarts it, keeping the login setting (always; `chi update` does it only when needed) |
|
|
184
227
|
| `chi desktop uninstall` | quits it and removes the app, its login item, launch file and settings |
|
|
185
|
-
| `chi desktop status` | version against chi's, how it runs chi, state dirs, Service, hotkey, process, login item |
|
|
228
|
+
| `chi desktop status` | version against chi's, how it runs chi, state dirs, Service, hotkey, process, login item, host keys |
|
|
186
229
|
|
|
187
230
|
`chi self` has a `desktop` line: `0.1.x (matches)`, `0.1.w (up to date for chi 0.1.x)` (an older build whose sources
|
|
188
231
|
haven't changed), `0.1.w (chi is 0.1.x: chi update)` (a rebuild is due) or `not installed`. `chi desktop status`
|
|
@@ -198,11 +241,26 @@ exports. So `install` writes `~/Library/Application Support/Chi Helper/launch.js
|
|
|
198
241
|
- only these variables, and only when they are set: `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, `GEM_HOME`, `GEM_PATH`,
|
|
199
242
|
`RUBYLIB`. No tokens and no `SAMAGOTCHI_*` settings go in;
|
|
200
243
|
- the settings the panel needs from config.yml, as chi read them then: the `kitty:` section (when
|
|
201
|
-
`kitty.listen_on` is set)
|
|
244
|
+
`kitty.listen_on` is set), the `tmux:` section (when `tmux.enabled`: the tmux binary and the socket dir resolved
|
|
245
|
+
then, and the agents) and `broadcast_timeout` (`broadcast.triage_deadline` + 10 s).
|
|
202
246
|
|
|
203
247
|
The file freezes the install shell's values. If you install with a temporary `XDG_STATE_HOME`, the helper keeps
|
|
204
248
|
using it. `status` prints the baked dirs.
|
|
205
249
|
|
|
250
|
+
**Host API keys** don't go in the launch file either: with the variable a host's `api_key_env:` names unset, chi
|
|
251
|
+
reads the key from the login Keychain (service `chi`, account the variable's name; see
|
|
252
|
+
[API keys from the Keychain](configuration.md#api-keys-from-the-keychain)). Store each key the helper's sessions
|
|
253
|
+
need once:
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
security add-generic-password -s chi -a FIREWORKS_API_KEY -w
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`chi desktop status` has a `host keys` row for the hosts the helper's chi reads (the baked `XDG_CONFIG_HOME`'s
|
|
260
|
+
config.yml): `FIREWORKS_API_KEY keychain` is fine; `WORK_API_KEY this shell only` (set in your terminal, not in
|
|
261
|
+
the Keychain) or `unset` comes with the `security` command to store it. `install` and `upgrade` warn about the
|
|
262
|
+
same keys. No row when no host has `api_key_env:`.
|
|
263
|
+
|
|
206
264
|
### The contract with chi
|
|
207
265
|
|
|
208
266
|
The helper uses only these commands, so it could ship on its own later:
|
|
@@ -235,4 +293,14 @@ broadcast` says the note may be partly delivered.
|
|
|
235
293
|
shortcuts.
|
|
236
294
|
- **"Send to chi" missing on images in Finder or Preview** after an upgrade: the Services cache still has the old
|
|
237
295
|
(text-only) entry; `/System/Library/CoreServices/pbs -update`, restart the helper, or log out and back in.
|
|
296
|
+
- **A new session from the panel fails with `set VAR`**, or the model picker lists a host as unreachable: the
|
|
297
|
+
helper's chi has no environment variables, so the key must be in the Keychain (`chi desktop status`, host keys).
|
|
298
|
+
`security add-generic-password -s chi -a VAR -w`, then send again; no restart needed. A session started in a
|
|
299
|
+
terminal has the terminal's variables, but once its worker exited, a send from the panel wakes it without them.
|
|
300
|
+
A stopgap until the next reboot: `launchctl setenv VAR value` (then restart the helper).
|
|
301
|
+
- **"tmux not found at …"** in the panel (or "no tmux found" from `chi update`): set `tmux.binary` to your tmux
|
|
302
|
+
(`which tmux` in a terminal), then `chi update`.
|
|
303
|
+
- **No tmux agents listed while they run:** check `tmux.enabled: true` and that `chi update` ran since; then compare
|
|
304
|
+
the launch file's `tmux.socket_dir` with your server's socket, `tmux display-message -p '#{socket_path}'`: it must
|
|
305
|
+
be in that dir (a different `TMUX_TMPDIR`, or a server started with `-S`, isn't seen).
|
|
238
306
|
- **"No live sessions":** start one with `chi` in a terminal; `chi sessions list --live --scope=all` shows the same list.
|
data/docs/guardrails.md
CHANGED
|
@@ -299,14 +299,14 @@ about with its arguments (`x: sum: a=20 b=22`), and an approval of "this
|
|
|
299
299
|
call" is keyed by them.
|
|
300
300
|
|
|
301
301
|
A plugin tool that runs other tools for the model (a dispatcher such as
|
|
302
|
-
`
|
|
302
|
+
`call_tool`) can say, in its `targets:`, which tool a call acts as (`acts_as:`)
|
|
303
303
|
and with which arguments (`args:`). A rule's `tool:` then matches the call's
|
|
304
|
-
own name **or** the tool it acts as: `tool: mcp_github_*` fires for
|
|
305
|
-
`
|
|
304
|
+
own name **or** the tool it acts as: `tool: mcp_github_*` fires for a
|
|
305
|
+
`call_tool` that acts as `mcp_github_merge_pull_request`, and `tool: call_tool`
|
|
306
306
|
fires whatever it acts as. The question names the tool it acts as (by the
|
|
307
307
|
`label:` its `targets:` gives, if any: display only) and shows its arguments, and an approval of "this call" is keyed by the real name, the
|
|
308
308
|
alias and those arguments (`/guardrails` lists it as
|
|
309
|
-
`
|
|
309
|
+
`call_tool>mcp_github_merge_pull_request:number=7 …`), so it never covers a
|
|
310
310
|
direct call of that tool, nor a direct call's approval it. A tool can't act as
|
|
311
311
|
one of chi's own tools or another bundle's tool: that `acts_as:` is dropped
|
|
312
312
|
(and logged as `plugin_acts_as_dropped`), and the call is matched by its own
|
|
@@ -315,7 +315,10 @@ name.
|
|
|
315
315
|
|
|
316
316
|
```yaml
|
|
317
317
|
- id: mcp-ask
|
|
318
|
-
|
|
318
|
+
# every MCP call (call_tool, acting as mcp_<server>_<tool>); not a find_tools search.
|
|
319
|
+
# Before the rename (mcp_call) it also matched that tool's own name, for a call whose
|
|
320
|
+
# tool resolved to nothing; call_tool refuses such a call anyway.
|
|
321
|
+
tool: "mcp_*"
|
|
319
322
|
verdict: ask
|
|
320
323
|
reason: an MCP server's tool
|
|
321
324
|
```
|
data/docs/hooks.md
CHANGED
|
@@ -534,8 +534,8 @@ stripped of surrounding whitespace, except file text, an edit's
|
|
|
534
534
|
A `write`'s `content:` is the file text; an `edit` carries `old_text:` and
|
|
535
535
|
`new_text:` (its `content:` is `""`). A plugin or MCP tool's call has its
|
|
536
536
|
arguments whole on `args:` (string keys) instead, and its `content:` is those
|
|
537
|
-
arguments as JSON. An MCP tool's call is
|
|
538
|
-
"
|
|
537
|
+
arguments as JSON. An MCP tool's call is chi's `call_tool` (its tool index's, which the mcp
|
|
538
|
+
bundle runs): `tool: "call_tool"`, with `args: {"tool" => "<server>/<tool>", "args" => {…}}`.
|
|
539
539
|
|
|
540
540
|
## Bundle Hooks (unified workflow bundle)
|
|
541
541
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Anthropic Messages (`api: anthropic`)
|
|
2
|
+
|
|
3
|
+
A host with `api: anthropic` (Anthropic itself, a gateway passing Claude through, a local server such as Splash,
|
|
4
|
+
Fireworks' Anthropic-compatible API) runs the same chat loop as `api: openai`: `LLM::Adapters::BY_API` maps both apis
|
|
5
|
+
to an adapter, `HostRegistry::HostEntry#chat?` is true for either, and `Engine#backend_for` picks `LLM::ChatLoop`
|
|
6
|
+
(engine.md, "The model↔tool loop"). Only the adapter differs: `LLM::AnthropicMessages` instead of `LLM::OpenAIChat`.
|
|
7
|
+
The user-facing settings (`dialect:`, `auth:`, `max_tokens`, the known limits) are in configuration.md.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
ChatLoop::Run#generate
|
|
11
|
+
→ ChatLoop#wire_messages # the OpenAI-shaped wire every chat host gets,
|
|
12
|
+
# plus anthropic_content on replayed model turns
|
|
13
|
+
→ AnthropicMessages#chat
|
|
14
|
+
→ #request_body # options as Messages takes them, breakpoints on or off
|
|
15
|
+
→ AnthropicWire.translate # system blocks + messages, cache_control placed
|
|
16
|
+
→ AnthropicWire.tools # function schemas → {name, description, input_schema}
|
|
17
|
+
→ POST {base}/v1/messages # LLM::HTTP: retries, cancel, first-token limit
|
|
18
|
+
→ AnthropicAssembly#add per SSE event # → ChatResponse (text, reasoning, tool_calls, usage,
|
|
19
|
+
# finish_reason, provider_content)
|
|
20
|
+
→ a 400 it can fix: #refusal_fix, then the same request again (once per kind)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Files
|
|
24
|
+
|
|
25
|
+
Paths are under `lib/samagotchi/llm/`.
|
|
26
|
+
|
|
27
|
+
| File | Role |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `adapters.rb` | `Adapters::BY_API` (`openai`, `anthropic`), `.for(entry)`, `.build(api:, …)` for side requests (`IdleClient`) |
|
|
30
|
+
| `anthropic_messages.rb` | `AnthropicMessages`: the request body, `#chat` (streamed or not), the 400 fixes, `#list_models`, `#thinking_fields`, `#replays_content?` |
|
|
31
|
+
| `anthropic_wire.rb` | `AnthropicWire`: ChatLoop's wire messages as Messages' `system` and `messages`, the two breakpoints, tool ids and names |
|
|
32
|
+
| `anthropic_assembly.rb` | `AnthropicAssembly`: a response from a whole body (`.from_message`) or stream events (`#add`), its usage in the OpenAI shape |
|
|
33
|
+
| `dialect.rb` | `Dialect` (`STANDARD`, `FIREWORKS`): what a provider's `/v1/messages` does differently |
|
|
34
|
+
| `prompt_cache.rb` | `PromptCache.control`: the `cache_control` a breakpoint carries (`cache.ttl`) |
|
|
35
|
+
| `chat_loop.rb` | `ChatLoop#replay_blocks`, `#reasoning_history`'s floor, `Run#with_thinking` (saves `anthropic_content`) |
|
|
36
|
+
|
|
37
|
+
## The adapter
|
|
38
|
+
|
|
39
|
+
`AnthropicMessages.for(entry)` takes the host's API root, key variable, `auth:`, `dialect:` and remoteness;
|
|
40
|
+
`HostRegistry#adapter_for` keeps one per host for the worker's life, so what it learns from a 400 (below) lasts that
|
|
41
|
+
long. Each request posts `{base}/v1/messages` with `anthropic-version: 2023-06-01`, the key in the header the host's
|
|
42
|
+
`auth:` names (else the dialect's: `x-api-key` on the standard one), and `Session-Id` (plus the dialect's
|
|
43
|
+
`session_header`) when the loop has a session id.
|
|
44
|
+
|
|
45
|
+
`#request_body` builds, in order: `model`, `max_tokens`, `system` (when there is one), `messages`, `stream`, `tools`
|
|
46
|
+
with `tool_choice: {type: "auto"}` (when any), then the loop's options. The options lose chi's OpenAI-only fields
|
|
47
|
+
(`DROPPED_OPTIONS`: `reasoning_effort`, `chat_template_kwargs`, `stream_options`, `prompt_cache_key`, `logprobs`,
|
|
48
|
+
`top_logprobs`, `max_completion_tokens`); configured sampling passes as written. `max_tokens` is the options'
|
|
49
|
+
(`default.max_tokens`), else the model list's `max_tokens` capped at `DEFAULT_MAX_TOKENS` (64000), and never over an
|
|
50
|
+
output cap a 400 named. `#with_budget_room` keeps `max_tokens` above a budget-form `budget_tokens` plus `MIN_BUDGET`.
|
|
51
|
+
|
|
52
|
+
`#thinking_fields(level)` is the thinking level on this wire: `off` is `thinking: {type: "disabled"}`; `low`, `medium`
|
|
53
|
+
and `high` are `thinking: {type: "adaptive", display: <dialect's>}` with `output_config: {effort: <level>}`; `default`
|
|
54
|
+
sends nothing. `#thinking_off_refused_fields` (low effort) is what `IdleClient` sends a side ask whose model refused
|
|
55
|
+
thinking off.
|
|
56
|
+
|
|
57
|
+
`#list_models` reads `GET {base}/v1/models` page by page (`limit=1000`, `after_id`, `has_more`/`last_id`, at most
|
|
58
|
+
`AdapterCommon::MAX_MODEL_PAGES`, as OpenAIChat does); `#window_of` takes `max_input_tokens`, else the OpenAI-shaped fields (`ModelListing`).
|
|
59
|
+
|
|
60
|
+
## Wire translation (`AnthropicWire.translate`)
|
|
61
|
+
|
|
62
|
+
ChatLoop builds the same OpenAI-shaped wire messages for every chat host (`#wire_messages`); the adapter translates
|
|
63
|
+
them:
|
|
64
|
+
|
|
65
|
+
- The head system message becomes the top-level `system`, as text blocks: with its `cache_split:` (from
|
|
66
|
+
`SystemPrompt#stable_length`) two blocks, the stable part first. A later system message (a reminder, a turn note,
|
|
67
|
+
the CONTEXT line) becomes user text: a mid-conversation system role is model-gated.
|
|
68
|
+
- An assistant message with saved blocks (`anthropic_content`) sends them verbatim, a `tool_use`'s name and id made
|
|
69
|
+
valid (`#valid_tool_use`); without them, a text block and its `tool_use` blocks (arguments parsed into the `input`
|
|
70
|
+
object, `{}` with a warning when they aren't a JSON object). An assistant message with neither is dropped.
|
|
71
|
+
- Tool messages become `tool_result` blocks in a user message. Ids are made what Messages takes
|
|
72
|
+
(`AnthropicWire.tool_id`: invalid characters replaced and a short hash appended, the same on both sides), and tool
|
|
73
|
+
names too (`.tool_name`), so a malformed one never fails every later request.
|
|
74
|
+
- User parts become text and `image` blocks (a data URL as base64, an http(s) URL as a url source).
|
|
75
|
+
- Consecutive messages of one role merge into one, its `tool_result` blocks first. Blank text blocks are never sent;
|
|
76
|
+
a user message left empty says `(empty)`. `reasoning_content` and `cache_split` never go out.
|
|
77
|
+
|
|
78
|
+
## The response (`AnthropicAssembly`)
|
|
79
|
+
|
|
80
|
+
A stream is read from its `data:` lines alone (a gateway that drops the `event:` lines works), by content-block
|
|
81
|
+
index: `message_start` (served model, usage), `content_block_start` (text, thinking, redacted_thinking, tool_use),
|
|
82
|
+
`content_block_delta` (`text_delta`, `thinking_delta`, `signature_delta`, `input_json_delta`), `message_delta`
|
|
83
|
+
(stop reason, `stop_details`, usage). `AnthropicMessages#chat_streamed` passes every event but `ping` to `on_delta`,
|
|
84
|
+
`message_start` and `message_delta` with the usage so far attached (`#with_usage`); a block's start or delta counts as shown for the first-token limit
|
|
85
|
+
(`.shows?`). A 200 whose body is a JSON error instead of a stream raises that error (`#raise_plain_error`).
|
|
86
|
+
|
|
87
|
+
`#response` is a `ChatResponse`:
|
|
88
|
+
|
|
89
|
+
- `provider_content`: the blocks as received (string keys). A blank text block is dropped (Messages refuses one sent
|
|
90
|
+
back); a `tool_use` input is an object; a thinking block always has a `signature` (`""` when none came).
|
|
91
|
+
- `finish_reason` from `FINISH_REASONS`: `end_turn`/`stop_sequence` → `stop`, `max_tokens` and
|
|
92
|
+
`model_context_window_exceeded` → `length`, `tool_use` → `tool_calls`, `refusal` → `refusal`; anything else as it is.
|
|
93
|
+
- Usage in the OpenAI shape the rest of chi reads (`TokenUsage`, `SessionMetrics`, `ContextStatus`, the web's
|
|
94
|
+
`ctx.js`): Messages counts `input_tokens` without the cache, so `prompt_tokens` is input + cache reads + cache
|
|
95
|
+
writes, with `cached_tokens` and `cache_write_tokens` under `prompt_tokens_details`. Every field is optional and the
|
|
96
|
+
later value wins; all zeros is no usage (`#usage_payload`: Fireworks' `message_start` carries only zeros).
|
|
97
|
+
|
|
98
|
+
## Replaying thinking blocks
|
|
99
|
+
|
|
100
|
+
Messages wants a model turn's blocks back as they came: a thinking block with its signature, in its place among the
|
|
101
|
+
text and `tool_use` blocks (a tool loop with thinking needs the last assistant turn's). So:
|
|
102
|
+
|
|
103
|
+
- `ChatLoop::Run#with_thinking` saves the response's `provider_content` on the model entry as `anthropic_content`,
|
|
104
|
+
beside `thinking:` (string keys, so a resumed session sends the same bytes).
|
|
105
|
+
- `AnthropicMessages#replays_content?` is true, and `ChatLoop#replay_blocks` puts those blocks on the wire message,
|
|
106
|
+
which `AnthropicWire` sends verbatim. Not for an interrupted entry (its partial blocks have no signature) nor for
|
|
107
|
+
one with an unpaired call (it goes as flattened text). Before the reasoning boundary (`#reasoning_boundary`) the
|
|
108
|
+
thinking blocks are left out, so only the front of the history loses them; the conversation's last model entry,
|
|
109
|
+
when it has tool calls, keeps them even there (a run stopped between steps left its `tool_use` last).
|
|
110
|
+
- `ChatLoop#reasoning_history` floors the level at `turn` on a replaying adapter: `drop` would remove the running
|
|
111
|
+
run's thinking, which Messages needs. For the same reason a reasoning 400 doesn't ask again without reasoning
|
|
112
|
+
(`Run#generate`: it would resend the same body).
|
|
113
|
+
- A 400 about a thinking block's signature (`BadRequest#thinking_signature?`, a Claude 5.x block bound to a
|
|
114
|
+
different conversation) fails the turn for now; configuration.md has the known limit.
|
|
115
|
+
|
|
116
|
+
## Cache breakpoints
|
|
117
|
+
|
|
118
|
+
`#request_body` asks `AnthropicWire.translate` for breakpoints (`cache: PromptCache.control`, carrying `ttl: "1h"`
|
|
119
|
+
under `cache.ttl: 1h`) only for a chat request (`purpose: "chat"`; a recap, `ctx.ask_model` or a triage sets none),
|
|
120
|
+
on a dialect with `cache_breakpoints`, and until the host refused `cache_control`. Unlike `PromptCache.mark` on the
|
|
121
|
+
OpenAI path, it doesn't look at the model id or remoteness. The two marks:
|
|
122
|
+
|
|
123
|
+
1. the first `system` block (with `cache_split:`, the stable part; the per-session tail goes after it unmarked), which
|
|
124
|
+
caches the tools and the system prompt for every session on that prefix;
|
|
125
|
+
2. the last user block that can carry one (`#mark_last`): never a thinking block, nor one that came from a later
|
|
126
|
+
system message, which moves every request.
|
|
127
|
+
|
|
128
|
+
The log line's `cache=` says `on`, `1h` or nothing (`#cache_summary`). prompt-caching.md has the cache rules.
|
|
129
|
+
|
|
130
|
+
## Dialects
|
|
131
|
+
|
|
132
|
+
`LLM::Dialect` is a host's `dialect:` (`Dialect.parse` warns once on an unknown name or on a host whose api isn't
|
|
133
|
+
`anthropic`): `auth` (the default key header), `thinking_display` (the `display` sent with adaptive or budget
|
|
134
|
+
thinking, nil for none), `cache_breakpoints` and `session_header`.
|
|
135
|
+
|
|
136
|
+
| Dialect | auth | thinking display | breakpoints | session header |
|
|
137
|
+
|---|---|---|---|---|
|
|
138
|
+
| `STANDARD` (no `dialect:`) | `x-api-key` | `summarized` | yes | none |
|
|
139
|
+
| `FIREWORKS` | Bearer | none | no (an automatic prefix cache) | `x-session-affinity` |
|
|
140
|
+
|
|
141
|
+
## 400 fallbacks
|
|
142
|
+
|
|
143
|
+
`AnthropicMessages#chat` handles the 400s it can fix itself, once each per request, and remembers the fix for the
|
|
144
|
+
adapter's life, so ChatLoop's broader paths never see them (`#refusal_fix`, logged under `http`; kept in an
|
|
145
|
+
`LLM::RefusalMemo`, locked, since the turn, side-ask and recap threads share the host's adapter):
|
|
146
|
+
|
|
147
|
+
| 400 about | Fix | Remembered |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `thinking.display` (`DISPLAY_RE`) | no `display` from now on (`#without_display`), the thinking kept | per host (`thinking_display_refused`) |
|
|
150
|
+
| adaptive thinking or its effort (`ADAPTIVE_REFUSED_RE`) | the budget form: `thinking: {type: "enabled", budget_tokens: BUDGETS[level]}`, no effort (`#budget_form`) | per model (`thinking_form_switched`) |
|
|
151
|
+
| `cache_control` | no breakpoints | per host (`cache_control_refused`) |
|
|
152
|
+
| `max_tokens: N > M` (`OUTPUT_CAP_RE`) | `max_tokens` capped at M | per model (`output_cap`) |
|
|
153
|
+
|
|
154
|
+
The display check comes before the adaptive one: its path (`thinking.adaptive.display`) names adaptive too. A context
|
|
155
|
+
overflow, a refused sampling field, a signature or a binding-controls 400 is not one of these: it goes on as it is,
|
|
156
|
+
to the loop's own handling (ChatLoop's sampling and thinking-fields fallbacks) or to the turn's failure.
|
|
@@ -18,7 +18,7 @@ Recommended workflow:
|
|
|
18
18
|
Behavior:
|
|
19
19
|
|
|
20
20
|
- `execute`, `task_create` and the `!cmd` shell bang run the command in a non-login shell: `/bin/sh -c` on Linux; on macOS, when `/bin/zsh` is there, `/bin/zsh --emulate sh +o ignore_braces +o sh_glob -o bash_rematch +o bsd_echo -c` (macOS's `/bin/sh` is bash 3.2, which can't parse an apostrophe in a heredoc inside `"$( )"`, the usual commit message shape). That mode reads no rc files and keeps POSIX word splitting, unmatched globs left as is, `{1..3}`, `[[ … =~ (…) ]]` with `BASH_REMATCH`, arrays, `$'…'`, `&>` and echo expanding `\t`. Bash-only builtins and variables (`shopt`, `BASH_VERSION`, `${!prefix*}`, `read -p`) are not there.
|
|
21
|
-
- Task metadata and output are persisted under `tmp/tasks
|
|
21
|
+
- Task metadata and output are persisted under `tmp/tasks/`: each task's `tmp/tasks/<id>/task.json` is a `TaskRecord` (`lib/samagotchi/tools/task_record.rb`, a value object: `.from_file` reads it, `#to_file` is the JSON object written, an update is a `#with`). Its keys keep their order; `stderr_path` and `handed_over_by` (a handed-over execute) and `stop_requested_by` (a stop under way or done) are left out until set. `bundles/slow-commands` reads these files on its own, so the shape is a contract.
|
|
22
22
|
- A task record keeps its process's pid and start time (`pid_started`). Before `task_stop` signals, the pid must still lead its own process group and must not have started at another time; otherwise the pid was reused by another process, nothing is signalled and the task ends as failed. A start time that can't be read at that moment (a failed `ps`) falls back to the group check, as does a record without `pid_started` (from an older chi). `task_get`, `task_list`, `task_wait`'s polls and the running-task lists (a cancelled turn's note, a worker's idle exit) make the same check, so a task whose pid was reused reads as exited (failed) at once, not as running until a `task_stop`; on macOS that is one `ps` per running task per check (~5 ms).
|
|
23
23
|
- A stop (`task_stop`, the user's Stop of an `execute`, the execute limit in kill mode) TERMs the task's process group, then KILLs it after a grace. The groups its processes started are stopped too: a GNU `timeout N cmd` (without `--foreground`) moves itself and its command to a group of its own. `ProcessGroup.stop` finds them with one `ps -A -o pid=,ppid=,pgid=` (~40 ms on macOS) before the TERM and again before the KILL, walking down from the group's members. A process whose parent already exited (reparented to init, e.g. a setsid'd `cmd &`, or a backgrounded `timeout N cmd &` after its shell exited) is not found and runs on.
|
|
24
24
|
- Task listing is workspace-scoped (current project only).
|
data/docs/internals/bundles.md
CHANGED
|
@@ -35,6 +35,8 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
35
35
|
| `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` |
|
|
36
36
|
| `Uninstaller`, `Trash`, `Status`, `Listing` | remove, keep removed files, `chi bundle status`, `chi bundle list` |
|
|
37
37
|
| `IndexUpdater`, `IndexSync` | the managed `index.md` lines (Installer; and the write/edit tools for memories) |
|
|
38
|
+
| `IndexSize` | one scope's memory index as the prompt carries it (tokens, lines, bytes): `memory.index_warn_tokens`' crossing note after a memory write, `chi self`'s summary, the prompt's figures (`SystemPrompt`) |
|
|
39
|
+
| `Placeholder` | `{{placeholder}}` patterns left in an installed memory, one line per file from `Installer#detect_placeholders`; a hint, never a gate |
|
|
38
40
|
|
|
39
41
|
Readers of the installed state: `Hooks::BundleLoader` (Engine), `GuardrailWiring#bundle_rules`, `Plugin::Loader`,
|
|
40
42
|
`ContextProviders` (scripts), `BundleNeeds` (index markers). Each goes through `Provenance.each_installed` / `#record`
|