samagotchi 0.64.0 → 0.66.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 +88 -1
- data/README.md +2 -1
- data/bin/chi +51 -2
- data/docs/architecture.md +20 -3
- data/docs/cli.md +36 -2
- data/docs/configuration.md +177 -8
- data/docs/context.md +4 -1
- data/docs/internals/engine.md +19 -4
- data/docs/internals/prompt-caching.md +10 -2
- data/docs/memory.md +1 -1
- data/docs/plugins.md +163 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +13 -2
- data/docs/sub-agent.md +3 -1
- data/lib/samagotchi/bridge.rb +11 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +18 -1
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +21 -0
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +464 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +131 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +92 -0
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +68 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +5 -5
- data/lib/samagotchi/bundles/coordinator/plugin.rb +24 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/delegated.md +2 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/cache_confirm.rb +9 -6
- data/lib/samagotchi/client.rb +4 -2
- data/lib/samagotchi/commands/registry.rb +25 -8
- data/lib/samagotchi/config.rb +13 -5
- data/lib/samagotchi/engine.rb +151 -11
- data/lib/samagotchi/generation_phase.rb +17 -0
- data/lib/samagotchi/host_config.rb +27 -6
- data/lib/samagotchi/host_registry.rb +34 -11
- data/lib/samagotchi/idle_client.rb +62 -25
- data/lib/samagotchi/idle_recap.rb +5 -2
- data/lib/samagotchi/idle_target.rb +30 -7
- data/lib/samagotchi/kernel_loop.rb +9 -1
- data/lib/samagotchi/llm/adapters.rb +41 -0
- data/lib/samagotchi/llm/anthropic_assembly.rb +212 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +416 -0
- data/lib/samagotchi/llm/anthropic_wire.rb +238 -0
- data/lib/samagotchi/llm/api_key.rb +26 -9
- data/lib/samagotchi/llm/chat_loop.rb +113 -14
- data/lib/samagotchi/llm/dialect.rb +57 -0
- data/lib/samagotchi/llm/errors.rb +81 -12
- data/lib/samagotchi/llm/model_listing.rb +111 -0
- data/lib/samagotchi/llm/model_result.rb +9 -2
- data/lib/samagotchi/llm/openai_chat.rb +46 -93
- data/lib/samagotchi/model_profile.rb +1 -1
- data/lib/samagotchi/plugin/api.rb +80 -7
- data/lib/samagotchi/plugin/context.rb +22 -1
- data/lib/samagotchi/plugin/loader.rb +3 -2
- data/lib/samagotchi/reasoning_history.rb +14 -4
- data/lib/samagotchi/recap_setup.rb +2 -1
- data/lib/samagotchi/self_report.rb +18 -28
- data/lib/samagotchi/send_command.rb +23 -7
- data/lib/samagotchi/session.rb +21 -3
- data/lib/samagotchi/session_commands.rb +16 -1
- data/lib/samagotchi/session_manager.rb +4 -1
- data/lib/samagotchi/session_metrics.rb +6 -11
- data/lib/samagotchi/session_setup.rb +14 -7
- data/lib/samagotchi/steer_cut.rb +4 -0
- data/lib/samagotchi/system_prompt.rb +16 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +19 -9
- data/lib/samagotchi/terminal_ui/attached_loop.rb +8 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -3
- data/lib/samagotchi/terminal_ui/status_row.rb +24 -3
- data/lib/samagotchi/terminal_ui.rb +18 -2
- data/lib/samagotchi/tool_runner.rb +13 -1
- data/lib/samagotchi/toolkit.rb +246 -0
- data/lib/samagotchi/toolkit_command.rb +117 -0
- data/lib/samagotchi/tools/registry.rb +43 -6
- data/lib/samagotchi/turn_note.rb +22 -5
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +14 -1
- data/lib/samagotchi/web/public/activity.js +3 -0
- data/lib/samagotchi/web/public/annotate_actions.js +109 -0
- data/lib/samagotchi/web/public/annotate_bar.js +84 -2
- data/lib/samagotchi/web/public/app.js +111 -20
- data/lib/samagotchi/web/public/commands.js +4 -2
- data/lib/samagotchi/web/public/delivery_line.js +228 -0
- data/lib/samagotchi/web/public/index.html +38 -0
- data/lib/samagotchi/web/public/info_bar.js +5 -3
- data/lib/samagotchi/web/public/live_turn.js +8 -0
- data/lib/samagotchi/web/public/session_state.js +3 -0
- data/lib/samagotchi/web/public/stage_model.js +27 -7
- data/lib/samagotchi/web/public/stage_view.js +32 -10
- data/lib/samagotchi/web/public/stream_handlers.js +21 -2
- data/lib/samagotchi/web/public/toolkit_chip.js +29 -0
- data/lib/samagotchi/web/public/turn_events.js +22 -8
- data/lib/samagotchi/web/public/turn_html.js +20 -4
- data/lib/samagotchi/web/public/turn_model.js +16 -9
- data/lib/samagotchi/web/public/turn_view.js +6 -4
- data/lib/samagotchi/worker.rb +2 -0
- metadata +17 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# pair: pair this Codex CLI session with a chi session (chi's codex-peer
|
|
3
|
+
# bundle). Run from Codex, which sets CODEX_THREAD_ID:
|
|
4
|
+
#
|
|
5
|
+
# sh <bundle dir>/scripts/pair <chi session id, prefix or chi web URL>
|
|
6
|
+
#
|
|
7
|
+
# It attaches two pushed sources to the chi session: codex-inbox, whose hint
|
|
8
|
+
# is this session's thread id (chi's codex_send tool runs
|
|
9
|
+
# `codex queue --thread <id>` with it), and codex-review, where Codex's
|
|
10
|
+
# answers go. Again later, it only refreshes the thread id: the push carries
|
|
11
|
+
# the new hint. CHI names the chi command (default: chi on the PATH).
|
|
12
|
+
#
|
|
13
|
+
# chi writes the session's context under its state folder, which Codex's
|
|
14
|
+
# default sandbox doesn't let it write: ~/.codex/config.toml needs
|
|
15
|
+
# [sandbox_workspace_write]
|
|
16
|
+
# writable_roots = ["<state>/samagotchi/context"]
|
|
17
|
+
# and a write refused says so.
|
|
18
|
+
set -eu
|
|
19
|
+
|
|
20
|
+
usage() {
|
|
21
|
+
echo "usage: pair <chi session id or URL (http://127.0.0.1:4567/#/s/<id>)>" >&2
|
|
22
|
+
exit 2
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[ $# -eq 1 ] || usage
|
|
26
|
+
case "$1" in
|
|
27
|
+
-h | --help) usage ;;
|
|
28
|
+
http://* | https://*)
|
|
29
|
+
id=$(printf '%s\n' "$1" | sed -n 's|.*/s/\([0-9A-Za-z-][0-9A-Za-z-]*\).*|\1|p')
|
|
30
|
+
[ -n "$id" ] || { echo "pair: no session id in $1 (a chi web URL ends in #/s/<id>)" >&2; exit 2; }
|
|
31
|
+
;;
|
|
32
|
+
*) id=$1 ;;
|
|
33
|
+
esac
|
|
34
|
+
|
|
35
|
+
thread=${CODEX_THREAD_ID:-}
|
|
36
|
+
if [ -z "$thread" ]; then
|
|
37
|
+
echo "pair: CODEX_THREAD_ID isn't set: run this from a Codex CLI session" >&2
|
|
38
|
+
exit 2
|
|
39
|
+
fi
|
|
40
|
+
chi=${CHI:-chi}
|
|
41
|
+
|
|
42
|
+
# Prints chi's error, and the writable_roots line when the sandbox refused.
|
|
43
|
+
fail() {
|
|
44
|
+
printf '%s\n' "$1" >&2
|
|
45
|
+
case "$1" in
|
|
46
|
+
*"Operation not permitted"* | *"Read-only file system"* | *"Permission denied"*)
|
|
47
|
+
state=${XDG_STATE_HOME:-$HOME/.local/state}
|
|
48
|
+
config='~/.codex/config.toml'
|
|
49
|
+
[ -z "${CODEX_HOME:-}" ] || config=$CODEX_HOME/config.toml
|
|
50
|
+
cat >&2 <<MSG
|
|
51
|
+
pair: Codex's sandbox can't write chi's state: add to $config
|
|
52
|
+
[sandbox_workspace_write]
|
|
53
|
+
writable_roots = ["$state/samagotchi/context"]
|
|
54
|
+
then restart Codex and pair again.
|
|
55
|
+
MSG
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
exit 1
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# A JSON string: backslashes, quotes, tabs and newlines escaped.
|
|
62
|
+
json() {
|
|
63
|
+
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' |
|
|
64
|
+
awk 'BEGIN { ORS = ""; printf "\"" } NR > 1 { print "\\n" } { print } END { printf "\"" }'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Attaches a pushed source, unless the session has one of that name already.
|
|
68
|
+
attach() {
|
|
69
|
+
name=$1
|
|
70
|
+
shift
|
|
71
|
+
if ! out=$($chi context add "$name" --push "$@" "$id" 2>&1); then
|
|
72
|
+
if ! $chi context show "$name" --json "$id" >/dev/null 2>&1; then
|
|
73
|
+
fail "$out"
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
attach codex-inbox --why "the Codex session paired with you: send it parts with codex_send" --hint "$thread"
|
|
79
|
+
text="Codex peer inbox
|
|
80
|
+
thread: $thread
|
|
81
|
+
codex cwd: $PWD
|
|
82
|
+
paired: $(date '+%Y-%m-%d %H:%M')
|
|
83
|
+
Send to it with the codex_send tool (your skill_codex_peer memory); its answers come as the codex-review context."
|
|
84
|
+
if ! out=$(printf '{"text":%s,"hint":%s,"summary":%s}\n' "$(json "$text")" "$(json "$thread")" \
|
|
85
|
+
"$(json "Codex paired from $PWD")" | $chi context push codex-inbox "$id" 2>&1 >/dev/null); then
|
|
86
|
+
fail "$out"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
attach codex-review --why "Codex's answers to what you sent it (codex-peer)" --hint "latest Codex reply"
|
|
90
|
+
|
|
91
|
+
echo "paired with chi $id (thread $thread)"
|
|
92
|
+
$chi context ls "$id"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Skill: codex peer (send a part to the paired Codex session)
|
|
2
|
+
|
|
3
|
+
A Codex CLI session the user works with can be paired with this chi
|
|
4
|
+
session. Codex pairs from its side (its `chi-peer` skill): it attaches the
|
|
5
|
+
`codex-inbox` context (its thread id) and `codex-review` (where its answers
|
|
6
|
+
come). Then `codex_send` sends it a part of the work, for a **validate**, as
|
|
7
|
+
**context**, or with a **question**.
|
|
8
|
+
|
|
9
|
+
This is not a fresh review run (a headless `codex exec` that starts from
|
|
10
|
+
nothing). It is a Codex that already knows the work: send it a part, not the
|
|
11
|
+
session.
|
|
12
|
+
|
|
13
|
+
## When
|
|
14
|
+
|
|
15
|
+
Only when the user asks: "send this to Codex", "validate this with Codex",
|
|
16
|
+
"give Codex this as context", "ask Codex whether…". Never after each turn,
|
|
17
|
+
never on your own initiative.
|
|
18
|
+
|
|
19
|
+
## Steps
|
|
20
|
+
|
|
21
|
+
1. Is `codex-inbox` attached (`context_read` with no name lists it)? If not,
|
|
22
|
+
say so and don't call the tool: the user pairs from Codex. Once:
|
|
23
|
+
`/codex install-skill` (it installs Codex's `chi-peer` skill); then they
|
|
24
|
+
tell Codex "pair with chi <this session's id>".
|
|
25
|
+
2. Pick exactly the part the user means (a plan step, a diff, a finding, your
|
|
26
|
+
last answer), not the whole conversation. Name the files, functions and
|
|
27
|
+
diff ranges Codex should look at, so it can check them itself.
|
|
28
|
+
3. Call `codex_send`:
|
|
29
|
+
- `kind`: `validate` (check this, answer with a verdict), `context` (for
|
|
30
|
+
Codex to know; it answers only if something is wrong) or `question`.
|
|
31
|
+
- `title`: one short line; Codex's answer names it.
|
|
32
|
+
- `text`: the part, then what to check or ask and what you are unsure of.
|
|
33
|
+
A long text goes to a file Codex reads; you don't have to do anything.
|
|
34
|
+
4. Tell the user it was sent, then go on. Don't wait, don't poll, don't send
|
|
35
|
+
it again.
|
|
36
|
+
5. A validate's answer comes as an update of the `codex-review` context and
|
|
37
|
+
wakes you. `context_read codex-review`, check that its title matches what
|
|
38
|
+
you sent, and act on it with the user: its last line is
|
|
39
|
+
`VERDICT: OK`, `VERDICT: CHANGES` or `VERDICT: UNSURE (why)`. Codex's
|
|
40
|
+
verdict is advice; the user decides.
|
|
41
|
+
|
|
42
|
+
## When it fails
|
|
43
|
+
|
|
44
|
+
- "that Codex session is gone": Codex has no record of that thread. Tell the
|
|
45
|
+
user to pair again from Codex. A Codex session that was only closed is not
|
|
46
|
+
"gone": the send succeeds and the message waits for it (below).
|
|
47
|
+
- "no codex on the PATH" or "no Codex CLI at …": the chi that runs this
|
|
48
|
+
session can't find `codex`; the user installs it or sets `CODEX=` to its
|
|
49
|
+
path where chi runs.
|
|
50
|
+
- "Codex didn't take the message in 10 s": the Codex side didn't answer the
|
|
51
|
+
queue; the send was stopped. Say so; don't retry in a loop.
|
|
52
|
+
- No answer to a validate after a while (the user asks where it is): say so
|
|
53
|
+
plainly. The Codex session may have been closed: the message waits for it;
|
|
54
|
+
the user runs `codex resume` there or pairs again from a new Codex
|
|
55
|
+
session. Codex may be mid-turn (a queued message runs after its turn), or
|
|
56
|
+
its sandbox can't write chi's state: its `~/.codex/config.toml` needs the
|
|
57
|
+
`writable_roots` line for chi's context folder (`/codex install-skill`
|
|
58
|
+
shows it), and Codex must be restarted after adding it.
|
|
59
|
+
|
|
60
|
+
## Don't
|
|
61
|
+
|
|
62
|
+
- Don't send secrets, tokens or whole conversations.
|
|
63
|
+
- Don't answer Codex's reply with a thanks or an acknowledgement: that starts
|
|
64
|
+
a loop. One send per request.
|
|
65
|
+
|
|
66
|
+
The user can send without you: `/codex validate <text>`,
|
|
67
|
+
`/codex context <text>`, `/codex last` (your last answer, as context), and
|
|
68
|
+
`/codex` shows whether a Codex is paired.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: coordinator
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.4.0
|
|
4
4
|
scope: system
|
|
5
|
-
description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, ask before each merge, a handoff memory to resume from), /coordinate <goal>, /coordinate resume, /coordinate end and /children"
|
|
5
|
+
description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, ask before each merge, a handoff memory to resume from), /coordinate <goal>, /coordinate resume, /coordinate end and /children; the coordinator toolkit (lean + delegate, delegate_result, list_sessions)"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
files:
|
|
8
8
|
skill_coordinator.md:
|
|
@@ -10,6 +10,6 @@ files:
|
|
|
10
10
|
description: "Coordinate parallel work: split it, delegate each task wait:false in its own worktree (cwd:), check each report, ask the user before each merge; resume from a handoff_* memory"
|
|
11
11
|
plugin:
|
|
12
12
|
file: plugin.rb
|
|
13
|
-
sha256: sha256:
|
|
14
|
-
# the
|
|
15
|
-
requires_chi: ">= 0.
|
|
13
|
+
sha256: sha256:caaddd24fbec2c66fa0e88937478b7ec7799bac7c98d5dc7f71ffc3fc227953a
|
|
14
|
+
# the coordinator toolkit needs chi.toolkit and ctx.use_toolkit (0.66.0)
|
|
15
|
+
requires_chi: ">= 0.66.0"
|
|
@@ -31,6 +31,13 @@
|
|
|
31
31
|
#
|
|
32
32
|
# A child is asked before it changes anything outside its worktree by chi
|
|
33
33
|
# itself (the core child-boundary guardrail), in every mode: nothing to set up.
|
|
34
|
+
#
|
|
35
|
+
# The coordinator toolkit is lean plus the tools that run children
|
|
36
|
+
# (delegate, delegate_result, list_sessions). /coordinate puts the session
|
|
37
|
+
# on it before it asks the model (ctx.use_toolkit), which chi allows only
|
|
38
|
+
# before the session's first model reply and outside a turn: otherwise it
|
|
39
|
+
# says to run /toolkit coordinator, unless the session declares delegate
|
|
40
|
+
# anyway.
|
|
34
41
|
class Plugin
|
|
35
42
|
CARD_ID = "children"
|
|
36
43
|
# A card takes 6 actions: Refresh and up to 5 Stops.
|
|
@@ -47,7 +54,11 @@ class Plugin
|
|
|
47
54
|
|
|
48
55
|
def initialize(_settings = {}); end
|
|
49
56
|
|
|
57
|
+
TOOLKIT = "coordinator"
|
|
58
|
+
TOOLKIT_TOOLS = %w[delegate delegate_result list_sessions].freeze
|
|
59
|
+
|
|
50
60
|
def register(chi)
|
|
61
|
+
chi.toolkit TOOLKIT, base: "lean", add: TOOLKIT_TOOLS
|
|
51
62
|
chi.command "/children", "this session's child sessions: state, branch, last reply; all, stop <id>",
|
|
52
63
|
anytime: true do |args, ctx|
|
|
53
64
|
children_command(args.to_s.strip, ctx)
|
|
@@ -179,12 +190,23 @@ class Plugin
|
|
|
179
190
|
end
|
|
180
191
|
|
|
181
192
|
def ask(ctx, request, done)
|
|
193
|
+
note = toolkit_note(ctx)
|
|
182
194
|
begin
|
|
183
195
|
ctx.sessions.send(ctx.session_id, request)
|
|
184
196
|
rescue Samagotchi::Plugin::Sessions::Error => e
|
|
185
|
-
return "/coordinate: #{e.message}. Send this yourself:\n\n#{request}"
|
|
197
|
+
return [note, "/coordinate: #{e.message}. Send this yourself:\n\n#{request}"].compact.join("\n")
|
|
186
198
|
end
|
|
187
|
-
done
|
|
199
|
+
[note, done].compact.join("\n")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Puts the session on the coordinator toolkit (ctx.use_toolkit); nil when
|
|
203
|
+
# it is on it now, or chi refused but the session declares delegate
|
|
204
|
+
# anyway (its toolkit resolved: full, a kit on full), else the line that
|
|
205
|
+
# says how.
|
|
206
|
+
def toolkit_note(ctx)
|
|
207
|
+
return nil if ctx.toolkit == TOOLKIT || ctx.use_toolkit(TOOLKIT) || ctx.declared_tools.include?("delegate")
|
|
208
|
+
|
|
209
|
+
"This session's toolkit (#{ctx.toolkit}) has no delegate: run /toolkit #{TOOLKIT} to give it the coordinator's tools."
|
|
188
210
|
end
|
|
189
211
|
|
|
190
212
|
def quote(text)
|
|
@@ -57,7 +57,7 @@ Only the nested form is read. An env name used as a top-level key (`SAMAGOTCHI_D
|
|
|
57
57
|
**Excluded maps** (YAML-only, not part of the flat registry; skipped by scalar loader):
|
|
58
58
|
|
|
59
59
|
- `model_aliases:` map of alias → model id (`ModelRef.parse`, through `HostRegistry#resolve`). Keys lowercased on write (`ConfigFile.write_model_alias!`). Values may be bare `model` or qualified `host:model` (hybrid; only `:` names a host, `org/model` is an id). Aliases apply once: never point an alias at another alias.
|
|
60
|
-
- `hosts:` map of `name → {host, port | url, transport, api, api_key_env, profile, first_token_timeout, vision, sampling, thinking, enabled, models}` (`ConfigFile.hosts_config`, `host_registry.rb` `HostEntry`). Names lowercased; `url:` (http/https, optional path) replaces host/port, never both; `api_key_env:` names the env var holding the API key (never write a key into config.yml); `transport` overrides `server.transport`; `first_token_timeout` (seconds, `0` = off; a negative or non-number warns and is ignored) overrides `server.first_token_timeout` for that host; `models:` (a map of model ids, or a plain list) declares ids the host serves whatever its `/v1/models` lists: no "doesn't list" warning, and a bare id routes there as if listed (`HostModel`); an entry may hold `price: {input, output, cache_read?, cache_write?}` in USD per 1M tokens (`ModelPrice`; an unset cache rate is the input rate), which estimates a cost the provider doesn't report or reports as 0 (`cost_estimate`, shown as `~$`), and `served: [ids] | any`, the models a gateway may answer that id with (no served-model warning for them); model settings stay in the root `models:`; a worker reads `hosts:` from the file as it starts (`SAMAGOTCHI_HOSTS_JSON` replaces it for a process it is set on; `SessionManager.spawn_options` unsets it for workers).
|
|
60
|
+
- `hosts:` map of `name → {host, port | url, transport, api, api_key_env, auth, dialect, profile, first_token_timeout, vision, sampling, thinking, enabled, models}` (`ConfigFile.hosts_config`, `host_registry.rb` `HostEntry`). Names lowercased; `url:` (http/https, optional path) replaces host/port, never both; `api_key_env:` names the env var holding the API key (never write a key into config.yml); `auth:` (`bearer|x-api-key`) is the header the key goes in (default `x-api-key` on `api: anthropic`, Bearer elsewhere); `dialect:` (`fireworks`, or `standard` for the default; `api: anthropic` only) adjusts the Messages wire for that provider (Bearer by default, no thinking `display`, no `cache_control`, `x-session-affinity`; `LLM::Dialect`, docs/configuration.md "Anthropic Messages hosts"); `transport` overrides `server.transport`; `first_token_timeout` (seconds, `0` = off; a negative or non-number warns and is ignored) overrides `server.first_token_timeout` for that host; `models:` (a map of model ids, or a plain list) declares ids the host serves whatever its `/v1/models` lists: no "doesn't list" warning, and a bare id routes there as if listed (`HostModel`); an entry may hold `price: {input, output, cache_read?, cache_write?}` in USD per 1M tokens (`ModelPrice`; an unset cache rate is the input rate), which estimates a cost the provider doesn't report or reports as 0 (`cost_estimate`, shown as `~$`), and `served: [ids] | any`, the models a gateway may answer that id with (no served-model warning for them); model settings stay in the root `models:`; a worker reads `hosts:` from the file as it starts (`SAMAGOTCHI_HOSTS_JSON` replaces it for a process it is set on; `SessionManager.spawn_options` unsets it for workers).
|
|
61
61
|
- `models:` map of model id or alias → `{profile, vision, sampling, thinking, reasoning_history}` (`ConfigFile.model_settings`). `thinking:` (here, on a host, or `thinking.level` for every model) is `off|low|medium|high|default` (`Thinking`; unquoted `off` works, `on` is not a level; `default` = send nothing). Precedence: the session's own level (`/thinking LEVEL`, saved in the session; `chi --thinking` on a session run, `chi send --new --thinking`) > `chi web --thinking`/`SAMAGOTCHI_THINKING_LEVEL` > `models:` > `hosts.<name>.thinking` > `thinking.level` in config.yml > `default`. A `sampling:` key wins over the fields a level sends (`null` drops one); `docs/configuration.md` "Thinking". `reasoning_history:` (here or on a host) is `drop|turn|all`, which of its reasoning a chat host's model gets back. Precedence: the session's own level (`/reasoning-history LEVEL`, saved in the session) > `SAMAGOTCHI_REASONING_HISTORY` > `models:` > `hosts.<name>.reasoning_history` > `turn`; `docs/configuration.md` "Reasoning history". `/thinking` and `/reasoning-history` ask before a change that restarts the prompt cache; `--yes` applies it (`chi send` commands apply directly). `sampling:` (here or on a host) is a map of request fields passed to the provider as written (`temperature`, `top_p`, `presence_penalty`, `repeat_penalty`, …; a model's fields win over its host's per field; `null` = don't send; chi's own fields like `max_tokens`/`stream` are refused with a warning; `docs/configuration.md` "Sampling"). Keys match case-insensitively. `profile` (here or on a host) is `qwen36|gemma4`: the raw prompt format for native hosts. Precedence: `--profile`/`SAMAGOTCHI_MODEL_PROFILE` > `models:` > `hosts.<name>.profile` > the llama.cpp server's chat template > the name (`qwen`/`gemma`) > `qwen36` (`ModelProfile.resolve`). Set one when a model's name hides its family (e.g. a Qwen fine-tune under another name on mlx, which has no template to read).
|
|
62
62
|
- `hooks:` map of `hooks_dir` + per-event lists `{path, on_error}` (`Hooks::Loader.load`). `hooks_dir` may start with `~`.
|
|
63
63
|
- `guardrails:` tool-call rules (`docs/guardrails.md`; read in `Engine#guardrail_rules`, parsed by `Guardrails::Rules.parse`): `enabled` (bool, default true; `false` drops rules and hooks' asks, a deny still applies; config.yml only, like every `guardrails` setting: no env variable), `parent_approvals` (`off`, the default: a parent agent's `chi answer` can only deny an approval; `once`: it may give "Allow once" too; a convention for an honest parent, not a security boundary, and the user's choice: set it only when the user asks), `rules:` (list), `disable:` (a rule id or a list of them, `id` or `bundle:id`, switching off a bundle's or config rule without editing it), `small_models` (`auto`, the default: small = 32B or less read from the name; a glob or list of globs; `[]` for none). A rule takes only `id`, `tool`, `command`, `path`, `git`, `models`, `verdict`, `reason`, `scopes`: `id` required; at least one of `tool` (a name, `shell` = execute + task_create, a `File.fnmatch` glob like `"mcp_*"`, or a list), `command` (a Ruby regex on the shell command), `path` (a glob, or `outside_repo`: outside the session's repo, tmp folders and memory files excepted), `git` (only `outside_repo`: a shell call runs commit/add/reset/… in another checkout); `models` (optional: `small`, a model-name glob, or a list; without it the rule is for every model); `verdict` `ask|deny`; `scopes` (for `ask`) a subset of `once, session, repo, rule`. **Any parse error (an unknown key, a bad regex, no verdict) makes chi deny every tool call** until fixed, so validate with `YAML.safe_load` and keep the list shape. Rules are read again on the next tool call after config.yml or a bundle's rules change: no restart needed. Installed bundles' rule files (`chi bundle install guardrails`) add to them; `/guardrails` lists what loaded.
|
|
@@ -128,7 +128,7 @@ A guardrail rule's `tool:` may be a glob (`tool: "mcp_*"`, verdict `ask`) to cov
|
|
|
128
128
|
## Validations
|
|
129
129
|
|
|
130
130
|
- **Model name** (`default.model` / `SAMAGOTCHI_DEFAULT_MODEL`): `ModelProfile.required_model_name` — non-empty string, otherwise harness fails fast at startup. Via `Config.get("default.model")`.
|
|
131
|
-
- **Host api** (`hosts.<name>.api`): `llama_cpp|mlx|omlx` (raw-prompt loop; also the transport)
|
|
131
|
+
- **Host api** (`hosts.<name>.api`): `llama_cpp|mlx|omlx` (raw-prompt loop; also the transport), `openai` (chat loop at `http://HOST:PORT/v1`) or `anthropic` (chat loop over the Messages API at `{url}/v1/messages`; without `url:`/`host:` it is `https://api.anthropic.com`). Absent: raw-prompt loop. It replaces the removed `backend` setting.
|
|
132
132
|
- **Transport** (`server.transport`): enum `llama_cpp|mlx|omlx`.
|
|
133
133
|
- **Profile** (`models.<id>.profile`, `hosts.<name>.profile`, `SAMAGOTCHI_MODEL_PROFILE`): enum `qwen36|gemma4`; an unknown one warns and is ignored.
|
|
134
134
|
- **Alias name** (`write_model_alias!`):
|
|
@@ -5,6 +5,7 @@ Another chi session (the parent) gave you this task. It reads only your last mes
|
|
|
5
5
|
- Finish the task in this turn if you can. End with one reply the parent can act on without asking back: the result first, then evidence (paths, commands run and what they printed), then anything you could not verify or finish.
|
|
6
6
|
- Do only what the task asks. A question ("why does this spec fail?", "what calls X?", "check whether…") gets a report, not file changes: say what you found and what change you would make, and leave making it to the parent. Edit files only when the task asks you to change, fix, add or write something, and then only the files that takes.
|
|
7
7
|
- The parent may send follow-up messages later; each one is a new turn in this same session, with what you did so far.
|
|
8
|
-
- Work in the directory you were started in: every cd, read, edit, test and commit stays in it (in a linked worktree, never the repository's other checkouts). Don't change config, install bundles
|
|
8
|
+
- Work in the directory you were started in: every cd, read, edit, test and commit stays in it (in a linked worktree, never the repository's other checkouts). Don't change config, install bundles or edit managed files unless the task asks for it.
|
|
9
|
+
- Don't write memories. When a memory or skill looks wrong, or the task's own instructions led you astray, say so in your reply: what and why. The parent has more context and decides the general fix.
|
|
9
10
|
- You can't delegate further. Don't start other chi sessions, and send notes only to the parent.
|
|
10
11
|
- If you are blocked, stop and say so in the reply with the exact question. Use ask_user_question only when the task says the user is watching.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.66.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
8
|
self_map.md: sha256:dfc19266259a9af1e0d769b7e0f67996b740b0c9921fdf3dadad973f6e5c144a
|
|
9
|
-
config_modification_protocol.md: sha256:
|
|
10
|
-
memory_guide.md: sha256:
|
|
11
|
-
delegated.md: sha256:
|
|
9
|
+
config_modification_protocol.md: sha256:301895777916dd998d8946aaa2e8f3a6e7f2b18eaf9362eef11471ae602d54ab
|
|
10
|
+
memory_guide.md: sha256:c48eea744db69e4f3e9bb415e9a03a8c78bbcaa1205ad3b1fb157a079f92d22e
|
|
11
|
+
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|
|
@@ -94,7 +94,7 @@ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and
|
|
|
94
94
|
- Integrity: each file's sha256 is recorded at install; a hook/plugin/rule file changed afterwards is not loaded (rules: every call denied) until reinstalled. It is an integrity check, not proof of authorship.
|
|
95
95
|
- Installing = trusting its Ruby code (hooks, plugin), like a gem. Install only copies; the code runs at the next session start (`Engine.new`), so a running worker needs a restart to pick it up.
|
|
96
96
|
- A bundle without `*.md` (btw, mcp, loop-guard) adds no line to the prompt's memory index.
|
|
97
|
-
- Profiles are shipped bundles with only `includes:` (bundle names): `core` (loop-guard, check-in, guardrails, slow-commands; `chi bootstrap` installs it) and `dev` (known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer). `chi bundle install core` installs the ones not installed and records them; a bundle the user uninstalled stays out on a later install, upgrade or `chi update`, which install only bundles new to the profile. `chi bundle uninstall core` removes its recorded bundles, then core (`docs/memory.md` "Bundle profiles").
|
|
97
|
+
- Profiles are shipped bundles with only `includes:` (bundle names): `core` (loop-guard, check-in, guardrails, slow-commands; `chi bootstrap` installs it) and `dev` (known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer, codex-peer). `chi bundle install core` installs the ones not installed and records them; a bundle the user uninstalled stays out on a later install, upgrade or `chi update`, which install only bundles new to the profile. `chi bundle uninstall core` removes its recorded bundles, then core (`docs/memory.md` "Bundle profiles").
|
|
98
98
|
|
|
99
99
|
### CLI — `chi bundle`
|
|
100
100
|
|
|
@@ -25,12 +25,15 @@ module Samagotchi
|
|
|
25
25
|
|
|
26
26
|
# A command's argument words with --yes / -y (anywhere) taken out.
|
|
27
27
|
# @param args [String] what follows the command name
|
|
28
|
-
# @
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
# @param downcase [Boolean] false keeps the words' case (/toolkit: a
|
|
29
|
+
# plugin's or MCP server's tool names can be mixed case)
|
|
30
|
+
# @return [Array(String, Boolean)] the rest, stripped (and downcased),
|
|
31
|
+
# and whether --yes was there
|
|
32
|
+
def split(args, downcase: true)
|
|
33
|
+
text = args.to_s.strip
|
|
34
|
+
words = (downcase ? text.downcase : text).split
|
|
35
|
+
flags, rest = words.partition { |word| YES_FLAGS.include?(word.downcase) }
|
|
36
|
+
[rest.join(" "), !flags.empty?]
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
# Whether a change must ask first: the effective level changes, the
|
data/lib/samagotchi/client.rb
CHANGED
|
@@ -195,9 +195,11 @@ module Samagotchi
|
|
|
195
195
|
# @param api_key_env [String, nil] the variable holding the host's API
|
|
196
196
|
# key (llama.cpp's --api-key), sent as a bearer token on every request;
|
|
197
197
|
# nil sends no Authorization header
|
|
198
|
+
# @param auth [Symbol, nil] the header the key goes in (LLM::ApiKey::SCHEMES;
|
|
199
|
+
# HostEntry#auth_scheme), nil: Bearer
|
|
198
200
|
# @param env [Hash] where the key variable is read
|
|
199
201
|
def initialize(host: nil, port: nil, open_timeout: nil, read_timeout: nil, transport: nil, sleeper: nil, scheme: nil,
|
|
200
|
-
first_token_timeout: nil, name: nil, api_key_env: nil, env: ENV)
|
|
202
|
+
first_token_timeout: nil, name: nil, api_key_env: nil, auth: nil, env: ENV)
|
|
201
203
|
# Unified config precedence: CLI > ENV > file > default (via Samagotchi::Config)
|
|
202
204
|
@host = host || Samagotchi::Config.get("server.host")
|
|
203
205
|
@port = (port || Samagotchi::Config.get("server.port")).to_i
|
|
@@ -217,7 +219,7 @@ module Samagotchi
|
|
|
217
219
|
@label = name.to_s.empty? ? @transport.label : name.to_s
|
|
218
220
|
@http = LLM::HTTP.new(label: @label, open_timeout: @open_timeout, read_timeout: @read_timeout,
|
|
219
221
|
sleeper: sleeper, first_token_timeout: first_token_timeout,
|
|
220
|
-
api_key: LLM::ApiKey.for(api_key_env, host: @label, env: env))
|
|
222
|
+
api_key: LLM::ApiKey.for(api_key_env, host: @label, env: env, scheme: auth))
|
|
221
223
|
end
|
|
222
224
|
|
|
223
225
|
# Seconds a completion may take to stream its first text, or nil.
|
|
@@ -15,6 +15,15 @@ module Samagotchi
|
|
|
15
15
|
# (:refuse, "busy").
|
|
16
16
|
MID_TURN = %i[anytime queue refuse].freeze
|
|
17
17
|
|
|
18
|
+
# A plugin's pill in the web's selection bar (chi.annotation,
|
|
19
|
+
# docs/plugins.md): it prepares +line+ (a command line of its entry)
|
|
20
|
+
# and +template+ filled from the selection, and fills the composer
|
|
21
|
+
# with it, or sends it at once (+sends+).
|
|
22
|
+
Annotation = Data.define(:label, :line, :template, :sends) do
|
|
23
|
+
# As a listing names it ({label:, line:, template:, send:}).
|
|
24
|
+
def to_h = { label: label, line: line, template: template, send: sends }
|
|
25
|
+
end
|
|
26
|
+
|
|
18
27
|
# @!attribute id [Symbol] the entry's key (:model, :rollback, …)
|
|
19
28
|
# @!attribute name [String] "/model", "!rollback", …
|
|
20
29
|
# @!attribute mid_turn [Symbol, #call] what a line of it does while a
|
|
@@ -28,7 +37,9 @@ module Samagotchi
|
|
|
28
37
|
# a built-in's with the line, and calls a bundle's with the text
|
|
29
38
|
# after the name
|
|
30
39
|
# @!attribute source [String] "core", or the bundle that added it
|
|
31
|
-
|
|
40
|
+
# @!attribute annotations [Array<Annotation>] its selection-bar pills
|
|
41
|
+
# (a bundle's command; empty for the rest)
|
|
42
|
+
Entry = Struct.new(:id, :name, :description, :mid_turn, :local, :uis, :match, :handler, :source, :annotations,
|
|
32
43
|
keyword_init: true) do
|
|
33
44
|
def match?(text) = match.call(text)
|
|
34
45
|
|
|
@@ -88,15 +99,16 @@ module Samagotchi
|
|
|
88
99
|
# @param anytime [Boolean] mid_turn: :anytime (a bundle's command)
|
|
89
100
|
# @param mid_turn [Symbol, #call, nil] see Entry; nil takes +anytime+
|
|
90
101
|
# @param match [#call, nil] the default matches the name alone or the
|
|
91
|
-
# name, a
|
|
102
|
+
# name, any whitespace (a newline too) and arguments
|
|
103
|
+
# @param annotations [Array<Annotation>] see Entry
|
|
92
104
|
# @return [Entry]
|
|
93
105
|
def register(name, description, id: nil, anytime: false, mid_turn: nil, local: false, uis: nil, match: nil,
|
|
94
|
-
source: "core", &handler)
|
|
106
|
+
source: "core", annotations: [], &handler)
|
|
95
107
|
raise ArgumentError, "command #{name} is already registered" if @entries.any? { |entry| entry.name == name }
|
|
96
108
|
|
|
97
109
|
entry = Entry.new(id: id || name.delete_prefix("/").to_sym, name: name, description: description,
|
|
98
110
|
mid_turn: mid_turn || (anytime ? :anytime : :refuse), local: local, uis: uis, match: match || default_match(name),
|
|
99
|
-
handler: handler, source: source)
|
|
111
|
+
handler: handler, source: source, annotations: annotations.dup.freeze)
|
|
100
112
|
@entries << entry
|
|
101
113
|
entry
|
|
102
114
|
end
|
|
@@ -162,11 +174,15 @@ module Samagotchi
|
|
|
162
174
|
# What a snapshot tells a UI without an Engine (see .from_listing).
|
|
163
175
|
# @return [Array<Hash>] {name:, description:, anytime:, mid_turn:,
|
|
164
176
|
# local:, uis:, source:} per entry, in registration order; anytime
|
|
165
|
-
# is true only when every line is (mid_turn "anytime")
|
|
177
|
+
# is true only when every line is (mid_turn "anytime"). An entry
|
|
178
|
+
# with annotations adds annotations: [Annotation#to_h] (the web's
|
|
179
|
+
# selection bar; other UIs ignore the key).
|
|
166
180
|
def listing
|
|
167
181
|
@entries.map do |entry|
|
|
168
|
-
{ name: entry.name, description: entry.description, anytime: entry.anytime,
|
|
169
|
-
|
|
182
|
+
item = { name: entry.name, description: entry.description, anytime: entry.anytime,
|
|
183
|
+
mid_turn: entry.mid_turn_label, local: entry.local ? true : false, uis: entry.uis&.map(&:to_s), source: entry.source }
|
|
184
|
+
item[:annotations] = entry.annotations.map(&:to_h) if entry.annotations&.any?
|
|
185
|
+
item
|
|
170
186
|
end
|
|
171
187
|
end
|
|
172
188
|
|
|
@@ -188,7 +204,8 @@ module Samagotchi
|
|
|
188
204
|
end
|
|
189
205
|
|
|
190
206
|
def default_match(name)
|
|
191
|
-
|
|
207
|
+
after = /\A#{Regexp.escape(name)}\s/
|
|
208
|
+
->(text) { text == name || after.match?(text) }
|
|
192
209
|
end
|
|
193
210
|
end
|
|
194
211
|
end
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -194,6 +194,12 @@ module Samagotchi
|
|
|
194
194
|
# the config surface.
|
|
195
195
|
Entry.new(key: "reasoning_history", yaml_path: %w[reasoning_history], type: :string, default: nil, expose: %i[env]),
|
|
196
196
|
|
|
197
|
+
# The toolkit a new session is stored on (Toolkit.configured): the tools
|
|
198
|
+
# its prompts declare. full (every built-in) when unset; child: a
|
|
199
|
+
# delegate child's. toolkit.kits (the user's own) is read by Toolkit.kits.
|
|
200
|
+
Entry.new(key: "toolkit.default", yaml_path: %w[toolkit default], type: :string, default: nil, expose: %i[env config]),
|
|
201
|
+
Entry.new(key: "toolkit.child", yaml_path: %w[toolkit child], type: :string, default: nil, expose: %i[env config]),
|
|
202
|
+
|
|
197
203
|
Entry.new(key: "default.max_tokens", yaml_path: %w[default max_tokens], type: :integer, default: nil, expose: %i[env config cli]),
|
|
198
204
|
Entry.new(key: "max_tool_output_chars", yaml_path: %w[max_tool_output_chars], type: :integer, default: 10_000, expose: %i[env config cli]),
|
|
199
205
|
|
|
@@ -279,7 +285,7 @@ module Samagotchi
|
|
|
279
285
|
# Maps of named entries and the keys an entry may hold
|
|
280
286
|
# (ConfigFile.hosts_config, ConfigFile.model_settings).
|
|
281
287
|
MAP_ENTRY_KEYS = {
|
|
282
|
-
"hosts" => %w[host port url transport api api_key_env profile first_token_timeout vision sampling thinking
|
|
288
|
+
"hosts" => %w[host port url transport api api_key_env auth dialect profile first_token_timeout vision sampling thinking
|
|
283
289
|
reasoning_history enabled remote window_tokens steer_cut_after llm_context_strategy llm_context_apply llm_context_budget_tokens models].freeze,
|
|
284
290
|
"models" => %w[profile vision sampling thinking reasoning_history window_tokens steer_cut_after llm_context_strategy llm_context_apply
|
|
285
291
|
llm_context_budget_tokens].freeze
|
|
@@ -287,7 +293,8 @@ module Samagotchi
|
|
|
287
293
|
# The keys a hosts.<name>.models entry may hold (HostModel.parse_map).
|
|
288
294
|
HOST_MODEL_KEYS = %w[price served].freeze
|
|
289
295
|
# Section keys beyond the registry's: guardrails' YAML rules (Engine#guardrail_rules).
|
|
290
|
-
|
|
296
|
+
# And toolkit's kits (Toolkit.kits).
|
|
297
|
+
SECTION_EXTRA_KEYS = { "guardrails" => %w[rules disable].freeze, "toolkit" => %w[kits].freeze }.freeze
|
|
291
298
|
|
|
292
299
|
# Fast lookup maps
|
|
293
300
|
BY_KEY = ENTRIES.each_with_object({}) { |e, h| h[e.key] = e }.freeze
|
|
@@ -851,9 +858,10 @@ module Samagotchi
|
|
|
851
858
|
|
|
852
859
|
HOSTS_KEY = "hosts"
|
|
853
860
|
VALID_TRANSPORTS_FOR_CONFIG = %w[llama_cpp mlx omlx].freeze
|
|
854
|
-
# How chi talks to a host: a raw-prompt api (also the Client transport)
|
|
855
|
-
#
|
|
856
|
-
|
|
861
|
+
# How chi talks to a host: a raw-prompt api (also the Client transport),
|
|
862
|
+
# or a chat api the chat loop talks (LLM::Adapters): openai (Chat
|
|
863
|
+
# Completions against <host>/v1) or anthropic (Messages).
|
|
864
|
+
VALID_APIS_FOR_CONFIG = (VALID_TRANSPORTS_FOR_CONFIG + %w[openai anthropic]).freeze
|
|
857
865
|
HOST_NAME_RE = /\A[a-z0-9][a-z0-9._-]*\z/i
|
|
858
866
|
ENV_NAME_RE = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
859
867
|
|