samagotchi 0.12.0 → 0.13.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 +62 -1
- data/README.md +20 -7
- data/bin/chi +5 -2
- data/docs/architecture.md +4 -2
- data/docs/cli.md +34 -10
- data/docs/configuration.md +29 -8
- data/docs/desktop.md +10 -6
- data/docs/guardrails.md +73 -7
- data/docs/hooks.md +13 -9
- data/docs/memory.md +5 -3
- data/docs/plugins.md +4 -4
- data/docs/sessions.md +9 -6
- data/docs/sub-agent.md +128 -0
- data/lib/samagotchi/answer_command.rb +220 -0
- data/lib/samagotchi/bridge.rb +11 -1
- data/lib/samagotchi/bridge_client.rb +8 -3
- data/lib/samagotchi/bundle_command.rb +18 -10
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +19 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +5 -5
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cli/command.rb +3 -2
- data/lib/samagotchi/cli/exit.rb +23 -0
- data/lib/samagotchi/cli/parent_wait.rb +82 -0
- data/lib/samagotchi/config.rb +50 -18
- data/lib/samagotchi/desktop_command.rb +5 -3
- data/lib/samagotchi/engine.rb +40 -14
- data/lib/samagotchi/guardrails/parent_approvals.rb +155 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +10 -3
- data/lib/samagotchi/host_registry.rb +0 -1
- data/lib/samagotchi/llm/openai_chat.rb +9 -1
- data/lib/samagotchi/memory_bundle/installer.rb +12 -0
- data/lib/samagotchi/memory_bundle/status.rb +9 -1
- data/lib/samagotchi/memory_paths.rb +8 -0
- data/lib/samagotchi/model_profile.rb +30 -16
- data/lib/samagotchi/parent_report.rb +213 -0
- data/lib/samagotchi/plugin/api.rb +15 -2
- data/lib/samagotchi/prompt_literal_guard.rb +4 -2
- data/lib/samagotchi/question_desk.rb +29 -2
- data/lib/samagotchi/recap_setup.rb +15 -5
- data/lib/samagotchi/reply_wait.rb +5 -1
- data/lib/samagotchi/send_command.rb +50 -75
- data/lib/samagotchi/session.rb +15 -0
- data/lib/samagotchi/session_manager.rb +34 -7
- data/lib/samagotchi/sessions_command.rb +13 -3
- data/lib/samagotchi/system_prompt.rb +10 -2
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +33 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -5
- data/lib/samagotchi/terminal_ui.rb +15 -1
- data/lib/samagotchi/thought_stream_splitter.rb +4 -4
- data/lib/samagotchi/tool_call_parser.rb +5 -6
- data/lib/samagotchi/tools/builtin_calls.rb +14 -3
- data/lib/samagotchi/tools/builtins.rb +14 -2
- data/lib/samagotchi/tools/delegate_wait.rb +16 -4
- data/lib/samagotchi/tools/execute.rb +8 -8
- data/lib/samagotchi/tools/task_create.rb +3 -2
- data/lib/samagotchi/tools/task_runtime.rb +7 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +4 -3
- data/lib/samagotchi/web/message_parts.rb +2 -1
- data/lib/samagotchi/web/server.rb +2 -1
- data/lib/samagotchi/web/session_summary.rb +3 -11
- metadata +7 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "cli/flags"
|
|
4
|
+
require_relative "cli/exit"
|
|
4
5
|
require_relative "memory_bundle"
|
|
5
6
|
|
|
6
7
|
module Samagotchi
|
|
@@ -109,7 +110,7 @@ module Samagotchi
|
|
|
109
110
|
when "build" then build(@argv[1..])
|
|
110
111
|
else
|
|
111
112
|
@stderr.puts "Unknown bundle subcommand: #{sub}. Use: install, upgrade, uninstall, status, diff, list, build"
|
|
112
|
-
return
|
|
113
|
+
return CLI::Exit::USAGE
|
|
113
114
|
end
|
|
114
115
|
end
|
|
115
116
|
|
|
@@ -124,7 +125,7 @@ module Samagotchi
|
|
|
124
125
|
force = opts.fetch(:force, false)
|
|
125
126
|
if source.nil? || source.empty?
|
|
126
127
|
@stderr.puts "Usage: chi bundle install <source> [--scope system|project] [--force]"
|
|
127
|
-
return
|
|
128
|
+
return CLI::Exit::USAGE
|
|
128
129
|
end
|
|
129
130
|
expanded_source = expand_source(source)
|
|
130
131
|
if Samagotchi::MemoryBundle::Profile.shipped_meta?(expanded_source)
|
|
@@ -148,7 +149,7 @@ module Samagotchi
|
|
|
148
149
|
agent = opts[:agent]
|
|
149
150
|
if source.nil? || source.empty?
|
|
150
151
|
@stderr.puts "Usage: chi bundle upgrade <source> [--scope system|project] [--force] [--dry-run]"
|
|
151
|
-
return
|
|
152
|
+
return CLI::Exit::USAGE
|
|
152
153
|
end
|
|
153
154
|
expanded_source = expand_source(source)
|
|
154
155
|
if Samagotchi::MemoryBundle::Profile.shipped_meta?(expanded_source)
|
|
@@ -217,7 +218,11 @@ module Samagotchi
|
|
|
217
218
|
|
|
218
219
|
# An upgrade that kept edited files: launch an agent to merge them
|
|
219
220
|
# (--agent, or yes on a terminal), or say how to take the bundle's.
|
|
220
|
-
#
|
|
221
|
+
# An upgrade that kept edited files exits 2, as a usage error does
|
|
222
|
+
# (scripts already read it so): not CLI::Exit::USAGE by meaning.
|
|
223
|
+
CONFLICTS_KEPT = 2
|
|
224
|
+
|
|
225
|
+
# @return [Integer] 0 resolved, CONFLICTS_KEPT kept the edits
|
|
221
226
|
def upgrade_conflicts(installer, bundle_name, expanded_source, agent)
|
|
222
227
|
@stdout.puts "\n#{installer.conflicts.size} conflict(s) need resolution."
|
|
223
228
|
installer.conflicts.each { |k, _| @stdout.puts " conflict: #{k}" }
|
|
@@ -227,13 +232,13 @@ module Samagotchi
|
|
|
227
232
|
ans && ans.strip.downcase.start_with?("y")
|
|
228
233
|
elsif agent.nil?
|
|
229
234
|
@stdout.puts "Non-interactive terminal: kept your edits in the file(s) above; the rest is upgraded. Re-run with --force to take the bundle's version, or --agent in a TTY to merge."
|
|
230
|
-
return
|
|
235
|
+
return CONFLICTS_KEPT
|
|
231
236
|
else
|
|
232
237
|
agent
|
|
233
238
|
end
|
|
234
239
|
unless launch
|
|
235
240
|
@stdout.puts "Kept your edits in the file(s) above; the rest is upgraded. chi bundle diff #{bundle_name} FILE shows the base; re-run with --force to take the bundle's version."
|
|
236
|
-
return
|
|
241
|
+
return CONFLICTS_KEPT
|
|
237
242
|
end
|
|
238
243
|
|
|
239
244
|
prompt = build_conflict_prompt(bundle_name, installer.conflicts, expanded_source)
|
|
@@ -258,7 +263,7 @@ module Samagotchi
|
|
|
258
263
|
force = opts.fetch(:force, false)
|
|
259
264
|
if bundle_name.nil? || bundle_name.empty?
|
|
260
265
|
@stderr.puts "Usage: chi bundle uninstall <bundle> [--scope system|project] [--force]"
|
|
261
|
-
return
|
|
266
|
+
return CLI::Exit::USAGE
|
|
262
267
|
end
|
|
263
268
|
data = Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).read
|
|
264
269
|
return uninstall_profile(bundle_name, force: force) if Samagotchi::MemoryBundle::Profile.installed_meta?(bundle_name, data)
|
|
@@ -355,6 +360,8 @@ module Samagotchi
|
|
|
355
360
|
exists = File.exist?(hook_path) ? "ok" : "missing"
|
|
356
361
|
@stdout.puts " #{hk}: event=#{event} on_error=#{on_error} priority=#{prio} [#{exists}]"
|
|
357
362
|
end
|
|
363
|
+
@stdout.puts " requires_chi: #{st[:provenance][:requires_chi]}" if st[:hooks_requires_failure]
|
|
364
|
+
@stdout.puts " not loaded: #{st[:hooks_requires_failure]}" if st[:hooks_requires_failure]
|
|
358
365
|
end
|
|
359
366
|
if (plugin = st[:plugin])
|
|
360
367
|
note = plugin[:state] == "modified" ? " (edited after install: not loaded; reinstall the bundle)" : ""
|
|
@@ -379,6 +386,7 @@ module Samagotchi
|
|
|
379
386
|
hook_info = hooks_count > 0 ? " hooks=#{hooks_count}" : ""
|
|
380
387
|
plugin = st[:plugin]
|
|
381
388
|
mods += 1 if plugin && (plugin[:state] != "ok" || plugin[:requires_failure])
|
|
389
|
+
mods += 1 if st[:hooks_requires_failure]
|
|
382
390
|
plugin_info = plugin ? " plugin=#{plugin[:file]}" : ""
|
|
383
391
|
includes = st[:provenance][:includes]
|
|
384
392
|
includes_info = includes ? " includes=#{includes.join(",")}" : ""
|
|
@@ -393,7 +401,7 @@ module Samagotchi
|
|
|
393
401
|
bname = args[0]
|
|
394
402
|
file_arg = args[1]
|
|
395
403
|
if bname.nil? || bname.empty?
|
|
396
|
-
@stderr.puts "Usage: chi bundle diff <bundle> [file]"; return
|
|
404
|
+
@stderr.puts "Usage: chi bundle diff <bundle> [file]"; return CLI::Exit::USAGE
|
|
397
405
|
end
|
|
398
406
|
prov = Samagotchi::MemoryBundle::Provenance.new(name: bname)
|
|
399
407
|
data = prov.read
|
|
@@ -534,7 +542,7 @@ module Samagotchi
|
|
|
534
542
|
# Validate scope if given
|
|
535
543
|
if scope && !%w[system project].include?(scope.to_s.strip.downcase)
|
|
536
544
|
@stderr.puts "Invalid scope '#{scope}', expected system or project"
|
|
537
|
-
return
|
|
545
|
+
return CLI::Exit::USAGE
|
|
538
546
|
end
|
|
539
547
|
begin
|
|
540
548
|
builder = Samagotchi::MemoryBundle::Builder.new(
|
|
@@ -573,7 +581,7 @@ module Samagotchi
|
|
|
573
581
|
end
|
|
574
582
|
if parsed.error
|
|
575
583
|
@stderr.puts "Unknown bundle #{sub} flag: #{parsed.error.arg}"
|
|
576
|
-
return
|
|
584
|
+
return CLI::Exit::USAGE
|
|
577
585
|
end
|
|
578
586
|
parsed
|
|
579
587
|
end
|
|
@@ -80,3 +80,22 @@ rules:
|
|
|
80
80
|
command: '(\.config/samagotchi|samagotchi/config\.yml|samagotchi/hooks|samagotchi/guardrails|memories/\.bundles|\.git/hooks)'
|
|
81
81
|
verdict: ask
|
|
82
82
|
reason: the command names chi's config, hooks, guardrails or git hooks
|
|
83
|
+
|
|
84
|
+
# ── answering chi's own questions around chi answer ───────────────────
|
|
85
|
+
# A parent agent answers a child's approval with chi answer, which holds it
|
|
86
|
+
# to guardrails.parent_approvals. These shapes go around that: chi --attach
|
|
87
|
+
# or chi -p with stdin from a pipe, a here-string or a file (a chi after 0.12 treats
|
|
88
|
+
# those as a parent too, but an older child doesn't), and a POST to the
|
|
89
|
+
# Bridge's or the web's answer route.
|
|
90
|
+
- id: chi-answer-piped
|
|
91
|
+
tool: shell
|
|
92
|
+
command: '(\|[^;&|\n]*\bchi\s([^;&|\n]*\s)?(--attach|--prompt|-p)\b|\bchi\s([^;&|\n]*\s)?(--attach|--prompt|-p)\b[^;&|\n]*<)'
|
|
93
|
+
verdict: ask
|
|
94
|
+
scopes: [once, session]
|
|
95
|
+
reason: answers chi's questions from a pipe or a file, around chi answer
|
|
96
|
+
- id: chi-answer-http
|
|
97
|
+
tool: shell
|
|
98
|
+
command: '\b(curl|wget|http|xh)\b[^;&|\n]*/answer\b'
|
|
99
|
+
verdict: ask
|
|
100
|
+
scopes: [once, session]
|
|
101
|
+
reason: posts an answer to a chi session's question directly, around chi answer
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: guardrails
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.3.0
|
|
4
4
|
scope: system
|
|
5
|
-
description: Optional default guardrail rules (git push/rewrites, rm -rf wide, curl | sh, writes outside the repo, git hooks), asks only small models get (git checkout --/restore, git stash drop), and a note for the model
|
|
5
|
+
description: Optional default guardrail rules (git push/rewrites, rm -rf wide, curl | sh, writes outside the repo, git hooks, answering chi around chi answer), asks only small models get (git checkout --/restore, git stash drop), and a note for the model
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
# models: in small-models.yml (guardrails.small_models)
|
|
8
8
|
requires_chi: ">= 0.8.1"
|
|
@@ -60,7 +60,7 @@ Only the nested form is read. An env name used as a top-level key (`SAMAGOTCHI_D
|
|
|
60
60
|
- `hosts:` map of `name → {host, port | url, transport, api, api_key_env, profile, first_token_timeout, vision, sampling, thinking, enabled}` (`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; workers inherit via `SAMAGOTCHI_HOSTS_JSON` (`hosts_json_for_env`, `session_manager.rb`).
|
|
61
61
|
- `models:` map of model id or alias → `{profile, vision, sampling, thinking}` (`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: `--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". `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
|
-
- `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; env `
|
|
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`, `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`); `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.
|
|
64
64
|
|
|
65
65
|
```yaml
|
|
66
66
|
guardrails:
|
|
@@ -122,7 +122,7 @@ A guardrail rule's `tool:` may be a glob (`tool: "mcp_*"`, verdict `ask`) to cov
|
|
|
122
122
|
3. Mutate the intended **nested** key in the raw hash. Preserve all other keys byte-for-byte where possible. Example for default model: `raw_data["default"] ||= {}; raw_data["default"]["model"] = "new-model"`.
|
|
123
123
|
4. **Validate** (see below) before writing. Also run `Samagotchi::Config.validate_yaml_sections` — it returns one `config: unknown key '…' (did you mean '…'?)` per key chi doesn't read (every config-exposed `Config::ENTRIES` key is known as written, including the section-less `max_tool_output_chars` and `skip_agent_md`; names under `hosts:`/`models:`/`model_aliases:`/`hooks:`/`bundles:`/`memories:` are free-form, host and model entries are checked against `Config::MAP_ENTRY_KEYS`). An empty list means no warning at start.
|
|
124
124
|
5. **Write**: change only the lines you mean to, with the edit tool, so the user's comments and layout stay; `YAML.dump` of the whole hash drops every comment, so rewrite the file only when it has none worth keeping. From Ruby, write atomically with `Samagotchi::AtomicFile.write(path, text)` (a unique temp file renamed over it), never through a fixed `"#{path}.tmp"` that another chi may be writing at the same time.
|
|
125
|
-
6. Nothing else to update: config.yml values are never copied into `ENV`, and every `Config.get` reads the file again when it changed, so the next read (and every worker started after the write) sees the new value with origin `:file`. What a running worker set up at its start (`hosts:`,
|
|
125
|
+
6. Nothing else to update: config.yml values are never copied into `ENV`, and every `Config.get` reads the file again when it changed, so the next read (and every worker started after the write) sees the new value with origin `:file`. What a running worker set up at its start (`hosts:`, bundle settings) waits for its restart; guardrail rules and `model_aliases:` are read again live. CLI overrides (`--model`, `--recap-model`, …) win over the file until the process exits; a worker gets its spawner's CLI settings through its env (`Config.cli_env`).
|
|
126
126
|
|
|
127
127
|
## Validations
|
|
128
128
|
|
|
@@ -136,8 +136,8 @@ A guardrail rule's `tool:` may be a glob (`tool: "mcp_*"`, verdict `ask`) to cov
|
|
|
136
136
|
- must not point to itself (case-insensitive)
|
|
137
137
|
- keys are normalized to downcase on write — `Qwen` and `qwen` collide
|
|
138
138
|
- **Alias target**: non-empty string (model id).
|
|
139
|
-
- **Hosts**: each entry needs `host
|
|
140
|
-
- **Hooks**: each entry must have `path` (relative to `hooks_dir`), `on_error` is `skip` (default) or `log
|
|
139
|
+
- **Hosts**: each entry needs `host` (or `url:`, an http(s) URL with an optional path, replacing `host`/`port`; never both), `port` 1-65535 (default 8080), `enabled: false` skips the entry, `transport` and `api` optional (a raw `api` must match `transport`), name must match `/\A[a-z0-9][a-z0-9._-]*\z/i`.
|
|
140
|
+
- **Hooks**: each entry must have `path` (relative to `hooks_dir`), `on_error` is `skip` (default) or `log`; `required: true` on a `before_tool_call` hook denies the call when the hook raises. Class name must match file basename snake→Pascal.
|
|
141
141
|
- **Scalars via registry**: `Config.coerce` validates `String/Numeric/true/false` per `type: :string/:integer/:float/:bool/:enum`; invalid values warn and fall back to entry `default`.
|
|
142
142
|
- **Keys**: `validate_yaml_sections` flags unknown keys (with a suggestion), a registry section that isn't a mapping, and env/CLI-only keys (`model.profile`).
|
|
143
143
|
|
|
@@ -151,5 +151,5 @@ A guardrail rule's `tool:` may be a glob (`tool: "mcp_*"`, verdict `ask`) to cov
|
|
|
151
151
|
|
|
152
152
|
- Precedence is `CLI > ENV > file > default` (`Config.resolve`; `Config.get_with_origin` names the layer). `ENV` holds only what the user (or a spawning chi's CLI flags) set, and CLI (`--recap-base-url`) wins over both via `Config.reload!(cli_overrides:)`.
|
|
153
153
|
- `--recap_base_url` (underscore) is rejected as unknown — use `--recap-base-url` (kebab). Same for all registry flags.
|
|
154
|
-
- `model_aliases`
|
|
154
|
+
- `model_aliases` are read again at each model resolution: an edit applies to the next `--model`, `/model`, new session or delegate, no restart. A running session keeps the model it already resolved.
|
|
155
155
|
- Keep edits minimal: touch only the key you intend to change; preserve `hosts:`/`hooks:`/`guardrails:`/`bundles:` maps. Adding a `bundles: <name>:` entry does not install the bundle (`chi bundle install <name>`).
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.13.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:93c788fadaf98adeb8946b4a357d2372bd6ebb439042d7cef8dda8fb44187315
|
|
9
|
-
config_modification_protocol.md: sha256:
|
|
9
|
+
config_modification_protocol.md: sha256:d0e9d4a84652bf54ec2fd664f61c536637af9decdd22fcd982f52195bf9f5fb4
|
|
10
10
|
memory_guide.md: sha256:81b911b9a015f0924c48162b3dc4b6376309e743765074419a99fab46e480858
|
|
11
11
|
delegated.md: sha256:0a71d5dfa7127df2d51cb5f0e7214240ed88777e36559430a8724c796d7ae561
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "exit"
|
|
4
|
+
|
|
3
5
|
module Samagotchi
|
|
4
6
|
module CLI
|
|
5
7
|
# The usage side of a subcommand: its error lines, usage errors (exit 2)
|
|
@@ -7,7 +9,6 @@ module Samagotchi
|
|
|
7
9
|
# #command_name ("chi send"); its usage text is USAGE unless it
|
|
8
10
|
# overrides #usage_text.
|
|
9
11
|
module Command
|
|
10
|
-
USAGE_EXIT = 2
|
|
11
12
|
HELP_WORDS = %w[-h --help help].freeze
|
|
12
13
|
|
|
13
14
|
private
|
|
@@ -30,7 +31,7 @@ module Samagotchi
|
|
|
30
31
|
usage = usage_on_error
|
|
31
32
|
error_line("#{command_name}: #{message}#{usage ? "" : " (see #{command_name} --help)"}")
|
|
32
33
|
@stderr.puts(usage) if usage
|
|
33
|
-
|
|
34
|
+
Exit::USAGE
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
# +argv+ through +flags+ (a Flags), help and usage errors done.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
module CLI
|
|
5
|
+
# chi's exit statuses, one table for every subcommand (docs/cli.md,
|
|
6
|
+
# docs/sub-agent.md): a script or a parent agent branches on them.
|
|
7
|
+
module Exit
|
|
8
|
+
OK = 0
|
|
9
|
+
# Anything that went wrong at run time: a refusal, a failed turn, a
|
|
10
|
+
# worker gone, a file that isn't there.
|
|
11
|
+
FAILED = 1
|
|
12
|
+
# The command line itself: an unknown flag, a missing argument, an
|
|
13
|
+
# option the question doesn't offer.
|
|
14
|
+
USAGE = 2
|
|
15
|
+
# A question waits for an answer (chi send --wait, chi answer, chi -p).
|
|
16
|
+
QUESTION = 3
|
|
17
|
+
# Still running after --timeout.
|
|
18
|
+
RUNNING = 4
|
|
19
|
+
# Ctrl-C: what a shell gives a command it interrupted.
|
|
20
|
+
INTERRUPTED = 130
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../session"
|
|
4
|
+
require_relative "../reply_wait"
|
|
5
|
+
require_relative "../parent_report"
|
|
6
|
+
require_relative "exit"
|
|
7
|
+
|
|
8
|
+
module Samagotchi
|
|
9
|
+
module CLI
|
|
10
|
+
# The parent-agent side shared by `chi send --wait` and `chi answer`:
|
|
11
|
+
# --format json's one object whatever the end, the session id resolved
|
|
12
|
+
# for it, and the wait for what comes next, reported the ParentReport
|
|
13
|
+
# way (Ctrl-C included). The including class (a CLI::Command) defines
|
|
14
|
+
# #parse (options, or an exit status) and #run_parsed(options), sets
|
|
15
|
+
# @stdout, @stderr and @state_dir, and may override the constants
|
|
16
|
+
# (specs stub them on the class).
|
|
17
|
+
module ParentWait
|
|
18
|
+
POLL_INTERVAL = ReplyWait::POLL_INTERVAL
|
|
19
|
+
# No live worker this long while waiting: it died before it could mark
|
|
20
|
+
# the session (a worker takes well under a second to start).
|
|
21
|
+
WORKER_GONE_AFTER = 5
|
|
22
|
+
FORMATS = %w[text json].freeze
|
|
23
|
+
|
|
24
|
+
# @return [Integer] the exit status
|
|
25
|
+
def run
|
|
26
|
+
options = parse
|
|
27
|
+
return options if options.is_a?(Integer)
|
|
28
|
+
|
|
29
|
+
@json = options[:format] == "json"
|
|
30
|
+
# With --format json stdout is one JSON object, whatever the end: a
|
|
31
|
+
# failure before the wait is reported from stderr's last line.
|
|
32
|
+
@stderr = ParentReport::LastLine.new(@stderr) if @json
|
|
33
|
+
status = run_parsed(options)
|
|
34
|
+
if @json && !@reported && status == 1
|
|
35
|
+
detail = @stderr.last.to_s.delete_prefix("#{command_name}: ")
|
|
36
|
+
@stdout.puts(ParentReport.error_line(detail, session_id: @session_id))
|
|
37
|
+
@stdout.flush
|
|
38
|
+
end
|
|
39
|
+
status
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# The full id of +given+ (an id or a prefix), kept for the JSON
|
|
45
|
+
# error line; nil after the error line.
|
|
46
|
+
# @return [String, nil]
|
|
47
|
+
def resolve(given)
|
|
48
|
+
id = Session.resolve_id(given, state_dir: @state_dir)
|
|
49
|
+
Session.load(id, state_dir: @state_dir)
|
|
50
|
+
@session_id = id
|
|
51
|
+
rescue ArgumentError => e
|
|
52
|
+
error_line("#{command_name}: #{e.is_a?(Session::AmbiguousId) ? e.message : "no session #{given}"}")
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Wait for the session's next reply past +cursor+ (ReplyWait) and
|
|
57
|
+
# report it. Ctrl-C leaves the turn running (130).
|
|
58
|
+
# @param owner_grace [Numeric, nil] seconds with no live worker before
|
|
59
|
+
# it counts as gone; nil waits for whatever wakes one
|
|
60
|
+
# @return [Integer] the exit status
|
|
61
|
+
def wait_for_reply(id, cursor:, baseline:, timeout:, owner_grace: self.class::WORKER_GONE_AFTER)
|
|
62
|
+
result = ReplyWait.call(id, state_dir: @state_dir, cursor: cursor, timeout: timeout, baseline: baseline,
|
|
63
|
+
owner_grace: owner_grace, poll_interval: self.class::POLL_INTERVAL)
|
|
64
|
+
result.text = result.text&.dup&.force_encoding(Encoding::UTF_8)&.scrub
|
|
65
|
+
@reported = true
|
|
66
|
+
ParentReport.report(result, session_id: id, stdout: @stdout, stderr: @stderr, command: command_name,
|
|
67
|
+
json: @json, timeout: timeout)
|
|
68
|
+
rescue Interrupt
|
|
69
|
+
if @json
|
|
70
|
+
@stdout.puts(ParentReport.json_line(ReplyWait::Result.new(status: :canceled), session_id: id))
|
|
71
|
+
@stdout.flush
|
|
72
|
+
@reported = true
|
|
73
|
+
end
|
|
74
|
+
error_line("#{command_name}: still running: chi --attach #{id}")
|
|
75
|
+
Exit::INTERRUPTED
|
|
76
|
+
rescue ArgumentError
|
|
77
|
+
error_line("#{command_name}: the session is gone (deleted while waiting)")
|
|
78
|
+
Exit::FAILED
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -104,10 +104,14 @@ module Samagotchi
|
|
|
104
104
|
Entry.new(key: "image.max_bytes", yaml_path: %w[image max_bytes], type: :integer, default: 3_750_000, expose: %i[env config]),
|
|
105
105
|
Entry.new(key: "image.max_per_request", yaml_path: %w[image max_per_request], type: :integer, default: 20, expose: %i[env config]),
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
# The guardrails settings are config.yml only: a parent agent must not switch a child's guardrails off from its
|
|
108
|
+
# own environment (SAMAGOTCHI_GUARDRAILS_ENABLED=false). SessionManager.spawn_options unsets them for a worker too.
|
|
109
|
+
Entry.new(key: "guardrails.enabled", yaml_path: %w[guardrails enabled], type: :bool, default: true, expose: %i[config]),
|
|
108
110
|
# Which models get the rules with `models: small` (Guardrails::ModelSize): auto (32B or less by the name's size, an MoE's
|
|
109
111
|
# active size counting), globs on the bare model name or its key, "|"-separated (a YAML list works too), or "" ([]) for none.
|
|
110
|
-
Entry.new(key: "guardrails.small_models", yaml_path: %w[guardrails small_models], type: :string, default: "auto", expose: %i[
|
|
112
|
+
Entry.new(key: "guardrails.small_models", yaml_path: %w[guardrails small_models], type: :string, default: "auto", expose: %i[config]),
|
|
113
|
+
# What `chi answer` lets a parent agent allow on an approval: off (deny only) or once ("Allow once", never a wider scope).
|
|
114
|
+
Entry.new(key: "guardrails.parent_approvals", yaml_path: %w[guardrails parent_approvals], type: :enum, default: "off", expose: %i[config], enum_values: %w[off once]),
|
|
111
115
|
|
|
112
116
|
Entry.new(key: "log.file", yaml_path: %w[log file], type: :string, default: nil, expose: %i[env config cli]),
|
|
113
117
|
Entry.new(key: "log.disable", yaml_path: %w[log disable], type: :bool, default: false, expose: %i[env config cli]),
|
|
@@ -156,6 +160,8 @@ module Samagotchi
|
|
|
156
160
|
Entry.new(key: "execute.truncate_at_bytes", yaml_path: %w[execute truncate_at_bytes], type: :integer, default: 65_536, expose: %i[env config cli]),
|
|
157
161
|
Entry.new(key: "execute.preview_bytes", yaml_path: %w[execute preview_bytes], type: :integer, default: 12_288, expose: %i[env config cli]),
|
|
158
162
|
Entry.new(key: "execute.telemetry_threshold_pct", yaml_path: %w[execute telemetry_threshold_pct], type: :integer, default: 80, expose: %i[env config cli]),
|
|
163
|
+
# Seconds one execute command may run before it is stopped (Tools::Execute::TIMEOUT_SEC).
|
|
164
|
+
Entry.new(key: "execute.timeout_sec", yaml_path: %w[execute timeout_sec], type: :integer, default: 30, expose: %i[env config cli]),
|
|
159
165
|
|
|
160
166
|
Entry.new(key: "web.port", yaml_path: %w[web port], type: :integer, default: 4567, expose: %i[env config cli]),
|
|
161
167
|
Entry.new(key: "web.host", yaml_path: %w[web host], type: :string, default: "127.0.0.1", expose: %i[env config cli]),
|
|
@@ -695,18 +701,7 @@ module Samagotchi
|
|
|
695
701
|
|
|
696
702
|
def hosts_config(env: ENV, path: global_path(env: env))
|
|
697
703
|
data = read_yaml(env: env, path: path)
|
|
698
|
-
raw_hosts = data
|
|
699
|
-
|
|
700
|
-
# ENV override: SAMAGOTCHI_HOSTS_JSON (used to propagate to workers)
|
|
701
|
-
env_json = env["SAMAGOTCHI_HOSTS_JSON"].to_s.strip
|
|
702
|
-
unless env_json.empty?
|
|
703
|
-
begin
|
|
704
|
-
parsed_env = JSON.parse(env_json)
|
|
705
|
-
raw_hosts = parsed_env if parsed_env.is_a?(Hash)
|
|
706
|
-
rescue StandardError
|
|
707
|
-
nil
|
|
708
|
-
end
|
|
709
|
-
end
|
|
704
|
+
raw_hosts = hosts_source(data, env)
|
|
710
705
|
|
|
711
706
|
normalized = {}
|
|
712
707
|
if raw_hosts.is_a?(Hash)
|
|
@@ -726,10 +721,8 @@ module Samagotchi
|
|
|
726
721
|
port = raw_cfg["port"] || raw_cfg[:port]
|
|
727
722
|
transport = raw_cfg["transport"] || raw_cfg[:transport]
|
|
728
723
|
api = raw_cfg["api"] || raw_cfg[:api]
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
next
|
|
732
|
-
end
|
|
724
|
+
next if host_disabled?(raw_cfg)
|
|
725
|
+
|
|
733
726
|
url = (raw_cfg["url"] || raw_cfg[:url]).to_s.strip
|
|
734
727
|
api_key_env = (raw_cfg["api_key_env"] || raw_cfg[:api_key_env]).to_s.strip
|
|
735
728
|
# Kept as written; ModelProfile.resolve warns about an unknown one.
|
|
@@ -822,6 +815,42 @@ module Samagotchi
|
|
|
822
815
|
{}
|
|
823
816
|
end
|
|
824
817
|
|
|
818
|
+
# The names (lowercased) of the hosts entries with enabled: false, which
|
|
819
|
+
# hosts_config leaves out. Read from the same source: config.yml's
|
|
820
|
+
# hosts:, or SAMAGOTCHI_HOSTS_JSON in a worker.
|
|
821
|
+
# @return [Array<String>]
|
|
822
|
+
def disabled_host_names(env: ENV, path: global_path(env: env))
|
|
823
|
+
raw = hosts_source(read_yaml(env: env, path: path), env)
|
|
824
|
+
return [] unless raw.is_a?(Hash)
|
|
825
|
+
|
|
826
|
+
raw.filter_map { |name, cfg| name.to_s.strip.downcase if cfg.is_a?(Hash) && host_disabled?(cfg) }
|
|
827
|
+
rescue StandardError
|
|
828
|
+
[]
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
# config.yml's hosts: as written, or the SAMAGOTCHI_HOSTS_JSON override
|
|
832
|
+
# a worker inherits from its parent.
|
|
833
|
+
def hosts_source(data, env)
|
|
834
|
+
raw = data[HOSTS_KEY] if data.is_a?(Hash)
|
|
835
|
+
env_json = env["SAMAGOTCHI_HOSTS_JSON"].to_s.strip
|
|
836
|
+
return raw if env_json.empty?
|
|
837
|
+
|
|
838
|
+
parsed = begin
|
|
839
|
+
JSON.parse(env_json)
|
|
840
|
+
rescue StandardError
|
|
841
|
+
nil
|
|
842
|
+
end
|
|
843
|
+
parsed.is_a?(Hash) ? parsed : raw
|
|
844
|
+
end
|
|
845
|
+
private_class_method :hosts_source
|
|
846
|
+
|
|
847
|
+
# enabled: false (or "false", any case) in a hosts entry.
|
|
848
|
+
def host_disabled?(cfg)
|
|
849
|
+
enabled = cfg.key?("enabled") ? cfg["enabled"] : (cfg.key?(:enabled) ? cfg[:enabled] : true)
|
|
850
|
+
enabled == false || enabled.to_s.strip.downcase == "false"
|
|
851
|
+
end
|
|
852
|
+
private_class_method :host_disabled?
|
|
853
|
+
|
|
825
854
|
# Resolve the `recap:` section through the Config registry so scalar
|
|
826
855
|
# recap settings share the single precedence path (CLI > ENV > file >
|
|
827
856
|
# default). Returns false when explicitly disabled (file `recap: false`
|
|
@@ -908,6 +937,9 @@ module Samagotchi
|
|
|
908
937
|
"profile" => v[:profile], "first_token_timeout" => v[:first_token_timeout],
|
|
909
938
|
"vision" => v[:vision], "sampling" => v[:sampling], "thinking" => v[:thinking]&.to_s).compact
|
|
910
939
|
end
|
|
940
|
+
# Disabled hosts travel as just that, so a worker refuses "box:x"
|
|
941
|
+
# the way its parent does instead of sending it to the default host.
|
|
942
|
+
disabled_host_names(env: env, path: path).grep(HOST_NAME_RE).each { |name| simple[name] ||= { "enabled" => false } }
|
|
911
943
|
JSON.generate(simple)
|
|
912
944
|
rescue StandardError
|
|
913
945
|
nil
|
|
@@ -6,7 +6,8 @@ require_relative "desktop"
|
|
|
6
6
|
|
|
7
7
|
module Samagotchi
|
|
8
8
|
# `chi desktop install|upgrade|uninstall|status`: the native helper that
|
|
9
|
-
# sends selected text to
|
|
9
|
+
# sends selected text (or images) to sessions, live, stopped or new, as a
|
|
10
|
+
# message or a context note (macOS for now).
|
|
10
11
|
class DesktopCommand
|
|
11
12
|
include CLI::Command
|
|
12
13
|
|
|
@@ -15,8 +16,9 @@ module Samagotchi
|
|
|
15
16
|
install [--force] [--login]
|
|
16
17
|
build "Chi Helper" into ~/Applications (needs the Command Line Tools)
|
|
17
18
|
and start it: Services > "Send to chi", or ⌃⌥⌘N with the clipboard,
|
|
18
|
-
sends text to a
|
|
19
|
-
|
|
19
|
+
sends text or images to a session (live, stopped or new) as a
|
|
20
|
+
message (⏎) or a context note (⌘⏎). --login: start it at login
|
|
21
|
+
too (else it runs until you log out)
|
|
20
22
|
upgrade rebuild it for this chi and restart it (chi update does this only
|
|
21
23
|
when its sources changed)
|
|
22
24
|
uninstall remove it, its settings and its launch file
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -223,8 +223,6 @@ module Samagotchi
|
|
|
223
223
|
# memory_read guard reads the same list for every turn.
|
|
224
224
|
@muted_memory_names = MutedMemories.normalize_list(muted_memories)
|
|
225
225
|
@kernel.muted_memory_names = @muted_memory_names
|
|
226
|
-
# Keep kernel client in sync with active host via setter
|
|
227
|
-
@kernel_client_synced = false
|
|
228
226
|
# ask_user_question blocks on the Engine's question flow (TUI/Web answer it).
|
|
229
227
|
@kernel.question_handler = proc { |payload| request_question(payload) }
|
|
230
228
|
# Every tool call asks this gate first. The kernel is never rebuilt, so
|
|
@@ -967,10 +965,6 @@ module Samagotchi
|
|
|
967
965
|
@prompt_builder.preloaded_memory_names
|
|
968
966
|
end
|
|
969
967
|
|
|
970
|
-
def memory_muted?(name)
|
|
971
|
-
MutedMemories.muted?(name, @muted_memory_names)
|
|
972
|
-
end
|
|
973
|
-
|
|
974
968
|
# ── Guardrails ─────────────────────────────────────────────────────────────
|
|
975
969
|
|
|
976
970
|
# Who can answer an approval: :repl, :worker or :non_interactive (the
|
|
@@ -1245,8 +1239,8 @@ module Samagotchi
|
|
|
1245
1239
|
|
|
1246
1240
|
# Answer the pending question (called from UI thread; QuestionDesk#answer).
|
|
1247
1241
|
# @return [Hash] normalized answer
|
|
1248
|
-
def answer_question(id:, selected:, freeform: nil)
|
|
1249
|
-
@question_desk.answer(id: id, selected: selected, freeform: freeform)
|
|
1242
|
+
def answer_question(id:, selected:, freeform: nil, client_id: nil)
|
|
1243
|
+
@question_desk.answer(id: id, selected: selected, freeform: freeform, client_id: client_id)
|
|
1250
1244
|
end
|
|
1251
1245
|
|
|
1252
1246
|
def set_question_sync_handler(&block)
|
|
@@ -1412,7 +1406,7 @@ module Samagotchi
|
|
|
1412
1406
|
# the prompt is kept in the session and :turn_canceled is emitted, then the
|
|
1413
1407
|
# Interrupt is re-raised so the caller still decides whether to exit. One
|
|
1414
1408
|
# after the turn's end event (in its after_turn/session_end hooks) is
|
|
1415
|
-
# only re-raised: the turn already ended. Any
|
|
1409
|
+
# only re-raised: the turn already ended; so is any error there. Any
|
|
1416
1410
|
# other error (e.g. an LLM::ProviderError) emits :turn_failed and
|
|
1417
1411
|
# re-raises; a provider error adds error_kind:, retryable:, host: and a
|
|
1418
1412
|
# one-line summary:.
|
|
@@ -1424,7 +1418,8 @@ module Samagotchi
|
|
|
1424
1418
|
# A Stop cuts the /props probes this thread makes for the turn
|
|
1425
1419
|
# (window, served model, vision) instead of waiting their timeout.
|
|
1426
1420
|
probe_cancel_before = Client.swap_probe_cancel(turn.controller)
|
|
1427
|
-
prepare_turn(turn, continue ? [] : images)
|
|
1421
|
+
return stopped_before_model(turn) unless prepare_turn(turn, continue ? [] : images)
|
|
1422
|
+
|
|
1428
1423
|
result = generate(turn, max_iterations: max_iterations, max_tool_output_chars: max_tool_output_chars,
|
|
1429
1424
|
pending_input: pending_input)
|
|
1430
1425
|
publish_used_memories(session, on_event)
|
|
@@ -1441,6 +1436,13 @@ module Samagotchi
|
|
|
1441
1436
|
end
|
|
1442
1437
|
raise
|
|
1443
1438
|
rescue StandardError => e
|
|
1439
|
+
# After the end event (post-turn work) likewise: logged and
|
|
1440
|
+
# re-raised, no turn_failed over the turn that ended.
|
|
1441
|
+
if turn.ended
|
|
1442
|
+
Log.warn(:turn, "post_turn_error", error: "#{e.class}: #{e.message}")
|
|
1443
|
+
raise
|
|
1444
|
+
end
|
|
1445
|
+
|
|
1444
1446
|
# Keep what the turn got to (the prompt plus the loop's completed
|
|
1445
1447
|
# tool iterations) like a cancel does, and save it: a worker exits
|
|
1446
1448
|
# after a failed turn. The REPL still rolls back to its checkpoint.
|
|
@@ -1482,7 +1484,9 @@ module Samagotchi
|
|
|
1482
1484
|
|
|
1483
1485
|
# Everything before the model is asked: :turn_started, the kernel's
|
|
1484
1486
|
# per-turn settings, the plugins' setup, the session_start/before_turn
|
|
1485
|
-
# hooks, then the messages to send (#turn_messages).
|
|
1487
|
+
# hooks, then the messages to send (#turn_messages). False when a Stop
|
|
1488
|
+
# came before the hooks (the turn ends canceled there; before the
|
|
1489
|
+
# load-failure warnings when it came during the probes), else true.
|
|
1486
1490
|
def prepare_turn(turn, images)
|
|
1487
1491
|
session = turn.session
|
|
1488
1492
|
image_refs, image_error = turn_image_refs(session, images)
|
|
@@ -1506,6 +1510,10 @@ module Samagotchi
|
|
|
1506
1510
|
# Before anything of the turn is kept or a reminder is used up.
|
|
1507
1511
|
turn.settings = turn_settings(session)
|
|
1508
1512
|
refuse_images!(turn.settings.vision) unless image_refs.empty?
|
|
1513
|
+
# A Stop during the probes: the turn ends before the load-failure
|
|
1514
|
+
# warnings, which show once, so the next turn still shows them.
|
|
1515
|
+
return false if turn.controller.cancelled?
|
|
1516
|
+
|
|
1509
1517
|
announce_guardrail_failures(turn.on_event)
|
|
1510
1518
|
# Plugins' slow setup that brings tools (an MCP server's first
|
|
1511
1519
|
# start): the turn waits for it here, before the system prompt
|
|
@@ -1515,6 +1523,9 @@ module Samagotchi
|
|
|
1515
1523
|
await_init_tasks(turn.controller, turn.on_event)
|
|
1516
1524
|
# Plugins' tool sets that changed since the last turn.
|
|
1517
1525
|
apply_staged_tools!
|
|
1526
|
+
# A Stop during the wait above: the turn ends here,
|
|
1527
|
+
# before the hooks run or a reminder is used up.
|
|
1528
|
+
return false if turn.controller.cancelled?
|
|
1518
1529
|
|
|
1519
1530
|
# Fire :session_start on the very first turn
|
|
1520
1531
|
if @first_turn
|
|
@@ -1528,9 +1539,22 @@ module Samagotchi
|
|
|
1528
1539
|
messages: hook_messages(session.messages) })
|
|
1529
1540
|
|
|
1530
1541
|
turn_messages(turn, image_refs)
|
|
1542
|
+
true
|
|
1531
1543
|
end
|
|
1532
1544
|
private :prepare_turn
|
|
1533
1545
|
|
|
1546
|
+
# A turn stopped before the model was asked (#prepare_turn returned
|
|
1547
|
+
# false): canceled with nothing kept, like a Ctrl-C there; the result
|
|
1548
|
+
# has no conversation, so a UI's checkpoint stands.
|
|
1549
|
+
def stopped_before_model(turn)
|
|
1550
|
+
reason = turn.controller.reason
|
|
1551
|
+
end_turn(turn, "canceled") do |seconds|
|
|
1552
|
+
[nil, { type: :turn_canceled, cancellation_reason: reason, duration_ms: (seconds * 1000).round }]
|
|
1553
|
+
end
|
|
1554
|
+
LLM::ModelResult.new(text: "", canceled: true, cancellation_reason: reason)
|
|
1555
|
+
end
|
|
1556
|
+
private :stopped_before_model
|
|
1557
|
+
|
|
1534
1558
|
# The turn's settings for the kernel (LLM::TurnSettings): vision,
|
|
1535
1559
|
# sampling and thinking, the thinking resolved before the hooks run.
|
|
1536
1560
|
# The model name is added in #generate, after the hooks that may switch
|
|
@@ -1640,7 +1664,8 @@ module Samagotchi
|
|
|
1640
1664
|
end
|
|
1641
1665
|
end
|
|
1642
1666
|
|
|
1643
|
-
# Fire :after_turn hook (
|
|
1667
|
+
# Fire :after_turn hook (an answer or a Stop; an Interrupt or a failure
|
|
1668
|
+
# ends the turn in #run_turn without it), with a read-only
|
|
1644
1669
|
# copy of the conversation the turn stored, and event[:present] for
|
|
1645
1670
|
# a display version of the answer (AnswerDisplay).
|
|
1646
1671
|
session = turn.session
|
|
@@ -1651,7 +1676,7 @@ module Samagotchi
|
|
|
1651
1676
|
@hooks.fire(:after_turn, after_turn)
|
|
1652
1677
|
store_answer_display(session, display, pending: display_pending)
|
|
1653
1678
|
|
|
1654
|
-
# Fire :session_end after
|
|
1679
|
+
# Fire :session_end after the turn (turn-level lifecycle), as :after_turn
|
|
1655
1680
|
@hooks.fire(:session_end, { type: :session_end, session_id: session.id })
|
|
1656
1681
|
end
|
|
1657
1682
|
private :complete_turn
|
|
@@ -1973,7 +1998,8 @@ module Samagotchi
|
|
|
1973
1998
|
end
|
|
1974
1999
|
begin
|
|
1975
2000
|
Hooks::BundleLoader.load(bundle_name: bundle_name, hooks_dir: hooks_dir, metadata: data[:hooks], registry: @hooks,
|
|
1976
|
-
failures: @guardrail_failures, settings: settings[bundle_name.to_s] || {}
|
|
2001
|
+
failures: @guardrail_failures, settings: settings[bundle_name.to_s] || {},
|
|
2002
|
+
requires_chi: data[:requires_chi])
|
|
1977
2003
|
rescue Exception => e
|
|
1978
2004
|
Log.error(:hooks, "bundle_load_failed", echo: "[samagotchi:hooks] bundle '#{bundle_name}' failed to load hooks: #{e.class}: #{e.message}", bundle: bundle_name, error: e.class.name)
|
|
1979
2005
|
end
|