samagotchi 0.154.1 → 0.156.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 +29 -1
- data/bin/chi +1 -1
- data/docs/architecture.md +12 -7
- data/docs/bundles.md +21 -0
- data/docs/cli.md +1 -1
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +5 -5
- data/docs/internals/attached-context.md +9 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/engine.md +642 -831
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +13 -6
- data/docs/internals/remote-sessions.md +3 -1
- data/docs/internals/web-frontend.md +1 -1
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +4 -4
- data/docs/system-one.md +3 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +7 -51
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/cli/parent_wait.rb +1 -1
- data/lib/samagotchi/config.rb +17 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_window.rb +3 -3
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/engine/context_judge.rb +21 -119
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +30 -80
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +26 -13
- data/lib/samagotchi/engine/toolkit.rb +35 -191
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +166 -98
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +28 -11
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +1 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +215 -313
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +87 -49
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/llm_context_notice.rb +1 -1
- data/lib/samagotchi/llm_context_strategy.rb +7 -9
- data/lib/samagotchi/llm_context_view.rb +9 -0
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/model_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +6 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/note_command.rb +1 -2
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/plugin/sessions.rb +2 -5
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +3 -3
- data/lib/samagotchi/reminder_carry.rb +1 -1
- data/lib/samagotchi/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +3 -3
- data/lib/samagotchi/self_report.rb +7 -7
- data/lib/samagotchi/session.rb +50 -7
- data/lib/samagotchi/session_access/local.rb +7 -5
- data/lib/samagotchi/session_access/remote.rb +17 -3
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +138 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +189 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +26 -1540
- data/lib/samagotchi/session_metrics.rb +1 -1
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +3 -3
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/system_prompt.rb +37 -30
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +2 -6
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +3 -3
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +15 -7
- data/lib/samagotchi/tool_declarations.rb +6 -6
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +96 -102
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/delegate.rb +10 -5
- data/lib/samagotchi/tools/delegate_result.rb +4 -4
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/registry.rb +64 -44
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +150 -102
- data/lib/samagotchi/tools/tool_surface.rb +187 -0
- data/lib/samagotchi/turn_context.rb +68 -0
- data/lib/samagotchi/turn_stages/context_judge.rb +172 -0
- data/lib/samagotchi/turn_stages/objectives.rb +150 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +257 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +168 -0
- data/lib/samagotchi/turn_stages/warmup.rb +82 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +3 -3
- data/lib/samagotchi/web/app.rb +15 -8
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +1 -1
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- data/lib/samagotchi/web/public/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +3 -3
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +3 -3
- data/lib/samagotchi/web/session_summary.rb +3 -4
- data/lib/samagotchi/worker.rb +55 -27
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +29 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The inline-widgets bundle (docs/bundles.md, The inline-widgets bundle): two
|
|
4
|
+
# prompt sections that teach the model the ```chi-ui blocks chi web draws
|
|
5
|
+
# (docs/internals/inline-widgets.md): picks, rows of options that fill the
|
|
6
|
+
# user's reply, and walk, a scenario stepped through. The drawing is core's; this
|
|
7
|
+
# bundle only says the blocks exist. Uninstalled, the prompt loses the sections
|
|
8
|
+
# and blocks already in a history still draw.
|
|
9
|
+
#
|
|
10
|
+
# The text is eval 0013's round-2 hint, split in two. It is static, so the
|
|
11
|
+
# prompt cache keeps: same text at every build. A delegate child's answer is
|
|
12
|
+
# read by its parent, not drawn for a user, and a one-shot (-p --non-interactive)
|
|
13
|
+
# prints to a terminal: neither gets a section.
|
|
14
|
+
class Plugin
|
|
15
|
+
PICKS = <<~TEXT
|
|
16
|
+
The web UI draws fenced ```chi-ui code blocks (YAML inside) as interactive widgets; the terminal shows their source. Use one whenever it helps the user more than prose: a widget replaces the long explanation, it doesn't sit beside one. Never use one for what markdown shows well (tables, lists, key-value). At most one widget per answer; keep the prose around it to a few sentences and don't repeat the widget's content.
|
|
17
|
+
|
|
18
|
+
picks: whenever your answer leaves the user two or more things to decide (review findings, a plan's open decisions, which of several fixes to do), even if they didn't ask you to list them. The user clicks one option per row, can add a note, and the UI fills their reply.
|
|
19
|
+
```chi-ui
|
|
20
|
+
type: picks
|
|
21
|
+
title: What to do with the findings
|
|
22
|
+
rows:
|
|
23
|
+
- id: "1" # unique, short; the user's reply names it
|
|
24
|
+
text: Bridge drops the replay buffer on reset # the item, <= 80 chars
|
|
25
|
+
options: # 2-4 short options, one per line
|
|
26
|
+
- fix now
|
|
27
|
+
- skip
|
|
28
|
+
- later
|
|
29
|
+
recommended: fix now # exactly one of the options
|
|
30
|
+
why: loses events a slow tab still needs # optional, one short line
|
|
31
|
+
```
|
|
32
|
+
Every row id unique; a row's options unique. Quote a value that starts with a quote or contains ": ".
|
|
33
|
+
TEXT
|
|
34
|
+
|
|
35
|
+
WALK = <<~TEXT
|
|
36
|
+
walk is the other ```chi-ui widget (the same rules: the web UI draws it, at most one widget per answer): a scenario the user steps through one step at a time: who does what, what the user sees, what happens behind it. Use it for "when does this happen / how does this work / what does the user go through" questions.
|
|
37
|
+
```chi-ui
|
|
38
|
+
type: walk
|
|
39
|
+
title: A tab reconnects
|
|
40
|
+
actors: [user, web, worker] # 2-5 short ids, unique
|
|
41
|
+
steps:
|
|
42
|
+
- id: s1 # unique
|
|
43
|
+
actor: user # one of actors
|
|
44
|
+
does: reopens the laptop # the action, <= 80 chars
|
|
45
|
+
sees: the page shows "reconnecting" # optional: what the user sees at this step
|
|
46
|
+
behind: the tab sends GET /events with Last-Event-ID # optional: what happens out of sight
|
|
47
|
+
- id: s2
|
|
48
|
+
actor: worker
|
|
49
|
+
does: checks the event id
|
|
50
|
+
cases: # optional: the step forks. List EVERY case, the usual one too
|
|
51
|
+
- when: the id is still buffered
|
|
52
|
+
goto: s3
|
|
53
|
+
- when: the id is too old
|
|
54
|
+
goto: s4
|
|
55
|
+
- id: s3
|
|
56
|
+
actor: worker
|
|
57
|
+
does: replays the missed events
|
|
58
|
+
next: end # optional: where to go after this step (default: the step below); end ends the walk here
|
|
59
|
+
- id: s4
|
|
60
|
+
actor: web
|
|
61
|
+
does: reloads the whole session
|
|
62
|
+
sees: the page redraws from the top
|
|
63
|
+
```
|
|
64
|
+
A step needs `does` or `sees` (or both). Steps run in order unless `next` or `cases` say otherwise. Every id unique; every goto names a step; a next names another step or is `end`. Quote a value that starts with a quote or contains ": ".
|
|
65
|
+
TEXT
|
|
66
|
+
|
|
67
|
+
def register(chi)
|
|
68
|
+
chi.prompt_section("picks") { |ctx| PICKS if drawn?(ctx) }
|
|
69
|
+
chi.prompt_section("walk") { |ctx| WALK if drawn?(ctx) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Whether the session's answers reach a web page that draws them.
|
|
75
|
+
def drawn?(ctx) = ctx.frontend != :one_shot && !ctx.delegate?
|
|
76
|
+
end
|
|
@@ -55,8 +55,8 @@ Only the nested form is read. An env name used as a top-level key (`SAMAGOTCHI_D
|
|
|
55
55
|
**Excluded maps** (YAML-only, not part of the flat registry; skipped by scalar loader):
|
|
56
56
|
|
|
57
57
|
- `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.
|
|
58
|
-
- `hosts:` map of `name → {host, port | url, api, api_key_env, auth, dialect, profile, first_token_timeout, vision, sampling, thinking, enabled, models}` (`
|
|
59
|
-
- `models:` map of model id or alias → `{profile, vision, sampling, thinking, reasoning_history}` (`
|
|
58
|
+
- `hosts:` map of `name → {host, port | url, api, api_key_env, auth, dialect, profile, first_token_timeout, vision, sampling, thinking, enabled, models}` (`HostConfig.all`, `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"); `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 (no environment variable sets hosts).
|
|
59
|
+
- `models:` map of model id or alias → `{profile, vision, sampling, thinking, reasoning_history}` (`ModelSettings.all`). `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 whose server publishes no template).
|
|
60
60
|
- `hooks:` map of `hooks_dir` + per-event lists `{path, on_error}` (`Hooks::Loader.load`). `hooks_dir` may start with `~`.
|
|
61
61
|
- `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_answers` (`soft`, the default: a parent agent's `chi answer` may deny, answer, Continue and give "Allow once"; `standard`: no allow; `hard`: deny, Stop and plain answers only; never a wider scope; replaces `parent_approvals` and `turn.parent_continue`; 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.
|
|
62
62
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.156.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:709bd52710bf6c50334a89a10742ea1554c5985cc2d7c4e7571e526cc4479526
|
|
9
|
-
config_modification_protocol.md: sha256:
|
|
9
|
+
config_modification_protocol.md: sha256:25ed2d4901f00eeea226db5eb2b75a05d6cdee4015ac8daa4bed624f745c3922
|
|
10
10
|
memory_guide.md: sha256:8b7727b939b9c04486ca59859561023dcc319a73e0f5b47b3fcf5741edaa69e2
|
|
11
11
|
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|
|
@@ -18,27 +18,22 @@ module Samagotchi
|
|
|
18
18
|
autoload :SessionContinue, File.expand_path("session_continue", __dir__)
|
|
19
19
|
|
|
20
20
|
# A continue moving the previous link's open delegates to the chain's new
|
|
21
|
-
# link (
|
|
21
|
+
# link (SessionContinue.run), instead of archiving them with
|
|
22
22
|
# it. A moved child gets a parent override (Session::PARENT_FILE), never a
|
|
23
23
|
# rewritten session file, so its live worker saving a stale copy changes
|
|
24
24
|
# nothing; its worker reads the override for its rings and at each turn's
|
|
25
25
|
# start (Bridge::FEATURES "reparent").
|
|
26
26
|
#
|
|
27
27
|
# #apply runs between the new link's save and its worker's spawn, while
|
|
28
|
-
# the link is
|
|
29
|
-
# second worker from the child's process until the link's
|
|
30
|
-
# Each step is idempotent,
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
28
|
+
# the link is marked starting (StartingMarker): a ring from a moved child
|
|
29
|
+
# must not spawn a second worker from the child's process until the link's
|
|
30
|
+
# own holds it. Each step is idempotent, and an intent file (INTENT_FILE)
|
|
31
|
+
# in the new link's folder names the move until it is done: a worker that
|
|
32
|
+
# starts and finds one finishes the move (#adopt), and a failed continue
|
|
33
|
+
# moves the same children back.
|
|
34
34
|
module ChildMove
|
|
35
35
|
# <new link dir>/move.json: {from, ids, noted}, while a move is under way.
|
|
36
36
|
INTENT_FILE = "move.json"
|
|
37
|
-
# <new link dir>/starting: {pid, at}, from the move to the worker's spawn.
|
|
38
|
-
STARTING_FILE = "starting"
|
|
39
|
-
# A starting marker older than this, or whose process is gone, is left
|
|
40
|
-
# over from a continue that died: it holds nothing back.
|
|
41
|
-
STARTING_STALE = 60.0
|
|
42
37
|
# Who the note to a moved child is from (ContextNote's label).
|
|
43
38
|
NOTE_SOURCE = "session chain"
|
|
44
39
|
# What a live child's worker must name in its sidecar to be moved.
|
|
@@ -206,44 +201,5 @@ module Samagotchi
|
|
|
206
201
|
end
|
|
207
202
|
SessionInbox.write_note(child_id, text: text, source: NOTE_SOURCE, state_dir: state_dir)
|
|
208
203
|
end
|
|
209
|
-
|
|
210
|
-
# Hold off a worker for the continue's new link (#starting?) until its
|
|
211
|
-
# own worker holds it: written before the move, removed by that worker
|
|
212
|
-
# once it has the owner lock (Worker#start).
|
|
213
|
-
def mark_starting(session_id, state_dir:)
|
|
214
|
-
dir = Session.session_dir(session_id, state_dir: state_dir)
|
|
215
|
-
FileUtils.mkdir_p(dir)
|
|
216
|
-
AtomicFile.write(File.join(dir, STARTING_FILE), JSON.generate({ "pid" => Process.pid, "at" => Time.now.iso8601(3) }))
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def clear_starting(session_id, state_dir:)
|
|
220
|
-
FileUtils.rm_f(File.join(Session.session_dir(session_id, state_dir: state_dir), STARTING_FILE))
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# Whether a continue is moving delegates to +session_id+ right now: its
|
|
224
|
-
# marker is there, fresh, and the process that wrote it runs.
|
|
225
|
-
def starting?(session_id, state_dir:)
|
|
226
|
-
file = File.join(Session.session_dir(session_id, state_dir: state_dir), STARTING_FILE)
|
|
227
|
-
return false unless File.exist?(file)
|
|
228
|
-
|
|
229
|
-
data = JSON.parse(File.read(file))
|
|
230
|
-
return false if Time.now - Time.iso8601(data["at"].to_s) > STARTING_STALE
|
|
231
|
-
|
|
232
|
-
process_alive?(data["pid"].to_i)
|
|
233
|
-
rescue JSON::ParserError, ArgumentError, SystemCallError
|
|
234
|
-
false
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# Signal 0 only asks whether the process exists.
|
|
238
|
-
def process_alive?(pid)
|
|
239
|
-
return false unless pid.positive?
|
|
240
|
-
|
|
241
|
-
Process.kill(0, pid)
|
|
242
|
-
true
|
|
243
|
-
rescue Errno::ESRCH
|
|
244
|
-
false
|
|
245
|
-
rescue Errno::EPERM
|
|
246
|
-
true
|
|
247
|
-
end
|
|
248
204
|
end
|
|
249
205
|
end
|
|
@@ -113,7 +113,7 @@ module Samagotchi
|
|
|
113
113
|
# session counts loaded itself.
|
|
114
114
|
def summary_of(session, state_dir:)
|
|
115
115
|
live = WorkerLink.resolve(Session.session_dir(session.id, state_dir: state_dir), unlink: false).worker_owned?
|
|
116
|
-
{ live: live, busy: live && session.status == Session::STATUS_RUNNING,
|
|
116
|
+
{ live: live, busy: live && session.status == Session::STATUS_RUNNING, saved_status: session.status,
|
|
117
117
|
updated_at: session.updated_at, waiting: session.waiting_question(live: live)&.dig(:kind),
|
|
118
118
|
preview: session.last_prompt.to_s }
|
|
119
119
|
end
|
|
@@ -55,7 +55,7 @@ module Samagotchi
|
|
|
55
55
|
def resolve(given)
|
|
56
56
|
@session_id = @access.resolve(given)
|
|
57
57
|
rescue ArgumentError => e
|
|
58
|
-
error_line("#{command_name}: #{e.
|
|
58
|
+
error_line("#{command_name}: #{e.message}")
|
|
59
59
|
nil
|
|
60
60
|
end
|
|
61
61
|
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -8,11 +8,14 @@ require_relative "config_text_edit"
|
|
|
8
8
|
require_relative "log"
|
|
9
9
|
require_relative "model_ref"
|
|
10
10
|
require_relative "paths"
|
|
11
|
-
require_relative "llm_context_strategy"
|
|
12
|
-
require_relative "host_model"
|
|
13
11
|
require_relative "config/restart_watch"
|
|
14
12
|
|
|
15
13
|
module Samagotchi
|
|
14
|
+
# Which LLM context strategy a turn runs under; it reads Config.
|
|
15
|
+
autoload :LLMContextStrategy, File.expand_path("llm_context_strategy", __dir__)
|
|
16
|
+
# A hosts.<name>.models entry and its price; they read ConfigFile.
|
|
17
|
+
autoload :HostModel, File.expand_path("host_model", __dir__)
|
|
18
|
+
autoload :ModelPrice, File.expand_path("model_price", __dir__)
|
|
16
19
|
# Parses the thinking: levels of host and model entries (it needs Config).
|
|
17
20
|
autoload :Thinking, File.expand_path("thinking", __dir__)
|
|
18
21
|
# One hosts: entry; it needs ConfigFile's readers.
|
|
@@ -89,6 +92,12 @@ module Samagotchi
|
|
|
89
92
|
next_history_access next_matching_turn_display_poll_write
|
|
90
93
|
].freeze
|
|
91
94
|
|
|
95
|
+
# llm_context.policy's default: the sentence forget_outputs' description
|
|
96
|
+
# carries (ToolDeclarations::FORGET_OUTPUTS_POLICY), the plan's D7 line,
|
|
97
|
+
# adapted from CLM's steering example.
|
|
98
|
+
DEFAULT_LLM_CONTEXT_POLICY = "Tidy at subtask boundaries: once a subtask is done, forget its tool outputs and note " \
|
|
99
|
+
"what it established; keep anything you'll still edit against."
|
|
100
|
+
|
|
92
101
|
ENTRIES = [
|
|
93
102
|
# universal – env+config+cli
|
|
94
103
|
Entry.new(key: "default.model", yaml_path: %w[default model], type: :string, default: nil, expose: %i[env config cli], lifecycle: :new_session),
|
|
@@ -261,7 +270,7 @@ module Samagotchi
|
|
|
261
270
|
# and hosts.<name>.llm_context_budget_tokens come first. Kept as written: LLMContextStrategy.parse_budget reads it.
|
|
262
271
|
Entry.new(key: "llm_context.budget_tokens", yaml_path: %w[llm_context budget_tokens], type: :string, default: nil, expose: %i[env config], lifecycle: :next_matching_turn_display_poll_write),
|
|
263
272
|
# The policy sentence forget_outputs' description carries (the forget layer); blank: none.
|
|
264
|
-
Entry.new(key: "llm_context.policy", yaml_path: %w[llm_context policy], type: :string, default:
|
|
273
|
+
Entry.new(key: "llm_context.policy", yaml_path: %w[llm_context policy], type: :string, default: DEFAULT_LLM_CONTEXT_POLICY, expose: %i[env config], lifecycle: :worker_start),
|
|
265
274
|
|
|
266
275
|
# How much models think: off|low|medium|high|default, parsed by Thinking
|
|
267
276
|
# (a :string, since YAML reads an unquoted off as false). The CLI flag
|
|
@@ -399,7 +408,7 @@ module Samagotchi
|
|
|
399
408
|
# appended_prompt_files, Hooks::Loader, ConfigFile.bundle_settings.
|
|
400
409
|
FREE_FORM_MAPS = %w[model_aliases hooks bundles memories append_system_prompt].freeze
|
|
401
410
|
# Maps of named entries and the keys an entry may hold
|
|
402
|
-
# (
|
|
411
|
+
# (HostConfig.all, ModelSettings.all).
|
|
403
412
|
MAP_ENTRY_KEYS = {
|
|
404
413
|
"hosts" => %w[host port url api api_key_env auth dialect profile first_token_timeout vision sampling thinking
|
|
405
414
|
reasoning_history enabled remote window_tokens steer_cut_after llm_context_strategy llm_context_apply llm_context_budget_tokens models].freeze,
|
|
@@ -408,6 +417,8 @@ module Samagotchi
|
|
|
408
417
|
}.freeze
|
|
409
418
|
# The keys a hosts.<name>.models entry may hold (HostModel.parse_map).
|
|
410
419
|
HOST_MODEL_KEYS = %w[price served].freeze
|
|
420
|
+
# The keys a hosts.<name>.models.<id>.price may hold (ModelPrice.parse).
|
|
421
|
+
MODEL_PRICE_KEYS = %w[input cache_read cache_write output].freeze
|
|
411
422
|
# Section keys beyond the registry's: guardrails' YAML rules (Engine#guardrail_rules).
|
|
412
423
|
# And toolkit's kits (Toolkit.kits), and bundles' judgments' settings (SystemOne::JudgmentSettings).
|
|
413
424
|
SECTION_EXTRA_KEYS = { "guardrails" => %w[rules disable].freeze, "toolkit" => %w[kits].freeze, "system_one" => %w[judgments].freeze }.freeze
|
|
@@ -887,9 +898,9 @@ module Samagotchi
|
|
|
887
898
|
def price_key_problems(prefix, price)
|
|
888
899
|
return [] unless price.is_a?(Hash)
|
|
889
900
|
|
|
890
|
-
(price.keys.map(&:to_s) -
|
|
901
|
+
(price.keys.map(&:to_s) - MODEL_PRICE_KEYS).map do |k|
|
|
891
902
|
# A renamed key is compared by its new name, so that name is the hint.
|
|
892
|
-
unknown_key_message("#{prefix}#{k}", RENAMED_PRICE_KEYS.fetch(k, k),
|
|
903
|
+
unknown_key_message("#{prefix}#{k}", RENAMED_PRICE_KEYS.fetch(k, k), MODEL_PRICE_KEYS, prefix: prefix)
|
|
893
904
|
end
|
|
894
905
|
end
|
|
895
906
|
|
|
@@ -1172,71 +1183,6 @@ module Samagotchi
|
|
|
1172
1183
|
HOST_NAME_RE = /\A[a-z0-9][a-z0-9._-]*\z/i
|
|
1173
1184
|
ENV_NAME_RE = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
1174
1185
|
|
|
1175
|
-
# config.yml's hosts: by lowercased name; an entry that is disabled or
|
|
1176
|
-
# invalid (warned) is left out. No entry left: "default", from
|
|
1177
|
-
# server.host/port.
|
|
1178
|
-
# @return [Hash{String => HostConfig}]
|
|
1179
|
-
def hosts_config(env: ENV, path: global_path(env: env))
|
|
1180
|
-
data = read_yaml(env: env, path: path)
|
|
1181
|
-
raw_hosts = data[HOSTS_KEY] if data.is_a?(Hash)
|
|
1182
|
-
|
|
1183
|
-
normalized = {}
|
|
1184
|
-
if raw_hosts.is_a?(Hash)
|
|
1185
|
-
raw_hosts.each do |raw_name, raw_cfg|
|
|
1186
|
-
host = HostConfig.parse(raw_name, raw_cfg)
|
|
1187
|
-
normalized[host.name] = host if host
|
|
1188
|
-
end
|
|
1189
|
-
warn_duplicate_host_models(normalized)
|
|
1190
|
-
end
|
|
1191
|
-
normalized["default"] = default_host_config(data, env) if normalized.empty?
|
|
1192
|
-
normalized
|
|
1193
|
-
rescue StandardError => e
|
|
1194
|
-
Log.error(:config, "hosts_config_failed", error: e.class.name, message: e.message)
|
|
1195
|
-
{}
|
|
1196
|
-
end
|
|
1197
|
-
|
|
1198
|
-
# The host chi uses when hosts: names none: server.host/port.
|
|
1199
|
-
def default_host_config(data, env)
|
|
1200
|
-
opts = { file_data: data, env: env, cli_overrides: Samagotchi::Config.cli_overrides }
|
|
1201
|
-
host = Samagotchi::Config.resolve("server.host", **opts).to_s.strip
|
|
1202
|
-
host = "localhost" if host.empty?
|
|
1203
|
-
port = Samagotchi::Config.resolve("server.port", **opts).to_i
|
|
1204
|
-
port = 8080 if port <= 0 || port > 65_535
|
|
1205
|
-
HostConfig.new(name: "default", host: host, port: port)
|
|
1206
|
-
end
|
|
1207
|
-
private_class_method :default_host_config
|
|
1208
|
-
|
|
1209
|
-
# Two hosts declaring one id under hosts.<name>.models: a bare id goes
|
|
1210
|
-
# where HostRegistry#host_for_model sends it (the default host when it
|
|
1211
|
-
# declares the id, else the first in hosts: order); say so once.
|
|
1212
|
-
def warn_duplicate_host_models(hosts)
|
|
1213
|
-
declared = Hash.new { |h, k| h[k] = [] }
|
|
1214
|
-
hosts.each { |name, cfg| cfg.models.each { |down, model| declared[down] << [name, model.id] } }
|
|
1215
|
-
declared.each_value do |owners|
|
|
1216
|
-
next if owners.size < 2
|
|
1217
|
-
|
|
1218
|
-
names = owners.map(&:first)
|
|
1219
|
-
id = owners.first.last
|
|
1220
|
-
goes = names.include?("default") ? "default" : "#{names.first} unless the default host lists it"
|
|
1221
|
-
warn_once "Warning: #{names.map { |n| "hosts.#{n}.models" }.join(" and ")} #{names.size == 2 ? "both" : "all"} declare #{id}; " \
|
|
1222
|
-
"a bare #{id} goes to #{goes}"
|
|
1223
|
-
end
|
|
1224
|
-
end
|
|
1225
|
-
private_class_method :warn_duplicate_host_models
|
|
1226
|
-
|
|
1227
|
-
# The names (lowercased) of the hosts entries with enabled: false, which
|
|
1228
|
-
# hosts_config leaves out, from config.yml's hosts:.
|
|
1229
|
-
# @return [Array<String>]
|
|
1230
|
-
def disabled_host_names(env: ENV, path: global_path(env: env))
|
|
1231
|
-
data = read_yaml(env: env, path: path)
|
|
1232
|
-
raw = data[HOSTS_KEY] if data.is_a?(Hash)
|
|
1233
|
-
return [] unless raw.is_a?(Hash)
|
|
1234
|
-
|
|
1235
|
-
raw.filter_map { |name, cfg| name.to_s.strip.downcase if cfg.is_a?(Hash) && HostConfig.disabled?(cfg) }
|
|
1236
|
-
rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
|
|
1237
|
-
[]
|
|
1238
|
-
end
|
|
1239
|
-
|
|
1240
1186
|
# Resolve the `recap:` section through the Config registry so scalar
|
|
1241
1187
|
# recap settings share the single precedence path (CLI > ENV > file >
|
|
1242
1188
|
# default). Returns false when explicitly disabled (file `recap: false`
|
|
@@ -1420,47 +1366,6 @@ module Samagotchi
|
|
|
1420
1366
|
{}
|
|
1421
1367
|
end
|
|
1422
1368
|
|
|
1423
|
-
# The top-level `models:` map: per-model settings keyed by model id or
|
|
1424
|
-
# alias (downcased), e.g. `models: {my-alias: {profile: qwen36}}`, each a
|
|
1425
|
-
# ModelSettings; an entry that is not a map is skipped.
|
|
1426
|
-
# @return [Hash{String => ModelSettings}]
|
|
1427
|
-
def model_settings(env: ENV, path: global_path(env: env))
|
|
1428
|
-
data = read_yaml(env: env, path: path)
|
|
1429
|
-
raw = data[MODELS_KEY] if data.is_a?(Hash)
|
|
1430
|
-
return {} unless raw.is_a?(Hash)
|
|
1431
|
-
|
|
1432
|
-
raw.each_with_object({}) do |(k, v), result|
|
|
1433
|
-
key = k.to_s.strip.downcase
|
|
1434
|
-
settings = ModelSettings.parse(key, v)
|
|
1435
|
-
result[key] = settings if settings
|
|
1436
|
-
end
|
|
1437
|
-
rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
|
|
1438
|
-
{}
|
|
1439
|
-
end
|
|
1440
|
-
|
|
1441
|
-
# The first +names+ (model as typed, alias-resolved, bare, …) whose
|
|
1442
|
-
# models: entry sets +field+ (a ModelSettings member; another name
|
|
1443
|
-
# raises ArgumentError): a ModelSetting (key, value), or nil.
|
|
1444
|
-
# VisionSupport and SamplingSettings look a model up the same way.
|
|
1445
|
-
# @param models [Hash{String => ModelSettings}, nil] model_settings by default
|
|
1446
|
-
# @return [ModelSetting, nil]
|
|
1447
|
-
def model_setting(names, field, models: nil)
|
|
1448
|
-
raise ArgumentError, "unknown models: field #{field.inspect}" unless ModelSettings.members.include?(field)
|
|
1449
|
-
|
|
1450
|
-
models ||= model_settings
|
|
1451
|
-
Array(names).map { |name| name.to_s.strip.downcase }.reject(&:empty?).uniq.each do |key|
|
|
1452
|
-
value = models[key]&.public_send(field)
|
|
1453
|
-
return ModelSetting.new(key: key, value: value) unless value.nil?
|
|
1454
|
-
end
|
|
1455
|
-
nil
|
|
1456
|
-
end
|
|
1457
|
-
|
|
1458
|
-
# +raw+ parsed against config.yml's hosts (or +hosts+) and aliases.
|
|
1459
|
-
# @return [ModelRef]
|
|
1460
|
-
def model_ref(raw, env: ENV, path: global_path(env: env), hosts: nil)
|
|
1461
|
-
ModelRef.parse(raw, hosts: hosts || hosts_config(env: env, path: path), aliases: model_aliases(env: env, path: path))
|
|
1462
|
-
end
|
|
1463
|
-
|
|
1464
1369
|
# Words /model takes as arguments (clear/default/none/off reset the
|
|
1465
1370
|
# runtime model), so no alias may be named after one.
|
|
1466
1371
|
RESERVED_MODEL_ALIASES = %w[clear default none off].freeze
|
|
@@ -428,8 +428,7 @@ module Samagotchi
|
|
|
428
428
|
session = @access.show(id)
|
|
429
429
|
Target.new(session_id: id, project_root: session.project_root, cwd: session.working_directory, label: id[0, 8])
|
|
430
430
|
rescue ArgumentError => e
|
|
431
|
-
|
|
432
|
-
error_line("#{command_name}: #{message}")
|
|
431
|
+
error_line("#{command_name}: #{e.message}")
|
|
433
432
|
nil
|
|
434
433
|
end
|
|
435
434
|
|
|
@@ -50,11 +50,11 @@ module Samagotchi
|
|
|
50
50
|
# and vision look a model up), else hosts.<name>.window_tokens.
|
|
51
51
|
# @param target [HostRegistry::ModelTarget]
|
|
52
52
|
# @param names [Array<String>] HostRegistry#lookup_names
|
|
53
|
-
# @param models [Hash{String => ModelSettings}, nil]
|
|
53
|
+
# @param models [Hash{String => ModelSettings}, nil] ModelSettings.all (specs)
|
|
54
54
|
# @return [Resolved, nil]
|
|
55
55
|
def setting(target, names:, models: nil)
|
|
56
|
-
models ||=
|
|
57
|
-
tokens =
|
|
56
|
+
models ||= ModelSettings.all
|
|
57
|
+
tokens = ModelSettings.find(names, :window_tokens, models: models)&.value
|
|
58
58
|
return Resolved.new(tokens: tokens, source: :model_setting) if positive_integer?(tokens)
|
|
59
59
|
|
|
60
60
|
tokens = target&.entry&.window_tokens
|
|
@@ -335,7 +335,7 @@ module Samagotchi
|
|
|
335
335
|
# are left out. @return [Array<Hash>] name:, hosts:, state:
|
|
336
336
|
def host_keys
|
|
337
337
|
config_env = { "XDG_CONFIG_HOME" => launch_config_home }
|
|
338
|
-
hosts =
|
|
338
|
+
hosts = HostConfig.all(env: config_env).values.select(&:api_key_env).group_by(&:api_key_env)
|
|
339
339
|
hosts.map do |name, entries|
|
|
340
340
|
state = if HostKeys.from_keychain(name, env: @env) then :keychain
|
|
341
341
|
elsif !@env[name].to_s.strip.empty? then :shell_only
|
|
@@ -2,137 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
module Samagotchi
|
|
4
4
|
class Engine
|
|
5
|
-
# The attached-context candidate judge (ContextJudge
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# The attached-context candidate judge (TurnStages::ContextJudge,
|
|
6
|
+
# forwarded): prepare_turn starts its pass, run_turn's ensure shows the
|
|
7
|
+
# cards it held, and a plugin's late candidate asks it. Reads
|
|
8
|
+
# @context_judge, @side_judgments, @host_registry, @turn_state, @session.
|
|
9
9
|
module ContextJudgePart
|
|
10
10
|
private
|
|
11
11
|
|
|
12
|
-
# The
|
|
13
|
-
|
|
14
|
-
# are logged (ContextCandidatesLog#look) and, unless
|
|
15
|
-
# context.candidates.mode is off, the proposed candidates due an ask
|
|
16
|
-
# are asked about the owner's prompts so far, this one included. Never
|
|
17
|
-
# a participant's turn, a review session or a Continue (no new prompt).
|
|
18
|
-
def start_context_judge(turn)
|
|
19
|
-
session = turn.session
|
|
20
|
-
return unless turn.prompt.is_a?(String) && OwnerPrompt.turn?(session: session, author: turn.origin.author)
|
|
21
|
-
|
|
22
|
-
context_judge_pass(session, ContextCandidates.owner_texts(session.messages) + [turn.prompt], phase: "turn")
|
|
23
|
-
end
|
|
24
|
-
private :start_context_judge
|
|
12
|
+
# The judge's stage (TurnStages::ContextJudge).
|
|
13
|
+
attr_reader :context_judge
|
|
25
14
|
|
|
26
15
|
# A late proposal no owner prompt mentions (Plugin::Host
|
|
27
|
-
# #candidate_proposed
|
|
28
|
-
#
|
|
16
|
+
# #candidate_proposed, maybe while the plugins load, before a session
|
|
17
|
+
# or the event log is there): TurnStages::ContextJudge#late_candidates
|
|
18
|
+
# on the session the Engine is on (none, or a review session: nothing,
|
|
19
|
+
# before anything else is read), the owner's prompts so far, the
|
|
20
|
+
# recorder and the sessions dir, all read now on the caller's thread.
|
|
29
21
|
def judge_late_candidates
|
|
30
22
|
session = @session
|
|
31
23
|
return if session.nil? || session.review?
|
|
32
24
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
end
|
|
36
|
-
private :judge_late_candidates
|
|
37
|
-
|
|
38
|
-
# One look at +session+'s candidates; the asks go to SideJudgments'
|
|
39
|
-
# :context kind, one thread asking each due candidate in turn.
|
|
40
|
-
def context_judge_pass(session, texts, phase:)
|
|
41
|
-
return if session.id.nil? || session.scratch
|
|
42
|
-
|
|
43
|
-
rows = ContextCandidates.with_status(session.id, project_root: session.project_root, state_dir: session_state_dir)
|
|
44
|
-
return if rows.empty?
|
|
45
|
-
|
|
46
|
-
log = ContextCandidatesLog.log_for(session_state_dir)
|
|
47
|
-
records = ContextCandidatesLog.records_for(log, session.id)
|
|
48
|
-
ContextCandidatesLog.look(log, session.id, rows, records) if phase == "turn"
|
|
49
|
-
settings = ContextJudge::Settings.from_config
|
|
50
|
-
return if settings.mode == "off"
|
|
51
|
-
|
|
52
|
-
jobs = ContextJudge.jobs(rows, records, session_id: session.id, texts: texts, settings: settings, phase: phase,
|
|
53
|
-
parent_attached: context_parent_attached(session))
|
|
54
|
-
return if jobs.empty?
|
|
55
|
-
|
|
56
|
-
client = system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :context),
|
|
57
|
-
ContextJudge::JUDGMENT, session, mode: settings.mode)
|
|
58
|
-
return unless client
|
|
59
|
-
|
|
60
|
-
start_context_asks(client, jobs, log, settings, context_choice(session, log, settings))
|
|
61
|
-
rescue StandardError => e
|
|
62
|
-
ContextJudge.warn(e)
|
|
25
|
+
@context_judge.late_candidates(session, messages: plugin_messages, recorder: system_one_recorder,
|
|
26
|
+
state_dir: session_state_dir)
|
|
63
27
|
end
|
|
64
|
-
private :context_judge_pass
|
|
65
|
-
|
|
66
|
-
# @return [Thread, nil] the asks' thread, nil when none started
|
|
67
|
-
def start_context_asks(client, jobs, log, settings, choice)
|
|
68
|
-
if @side_judgments.in_flight?(:context)
|
|
69
|
-
Log.info(:context, "context_judge_in_flight_skipped", session_id: jobs.first.session_id)
|
|
70
|
-
return nil
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
@side_judgments.start(:context, warn: ContextJudge.method(:warn)) do
|
|
74
|
-
jobs.each { |job| choice.call(job, ContextJudge.ask(client: client, job: job, log: log, settings: settings)) }
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
private :start_context_asks
|
|
78
|
-
|
|
79
|
-
# What an answer does on the asks' thread (ContextCandidateChoice
|
|
80
|
-
# #after_judged: on mode attaches or cards; audit nothing). Taken on
|
|
81
|
-
# the caller's thread. The attach only writes the session's folder: its
|
|
82
|
-
# note comes with the next between-turn delivery, never into a running
|
|
83
|
-
# turn. A card shows only while this Engine is still on that session,
|
|
84
|
-
# and not in a running turn (it isn't the turn's: no "waiting for you"
|
|
85
|
-
# on it), but once it has ended.
|
|
86
|
-
# @return [#call] (job, fields)
|
|
87
|
-
def context_choice(session, log, settings)
|
|
88
|
-
session_id = session.id
|
|
89
|
-
project_root = session.project_root
|
|
90
|
-
state_dir = session_state_dir
|
|
91
|
-
show = lambda do |**card|
|
|
92
|
-
current = @session
|
|
93
|
-
next nil unless current.nil? || current.id == session_id
|
|
94
|
-
|
|
95
|
-
@context_cards_mutex.synchronize do
|
|
96
|
-
next @context_cards_after_turn << card if turn_running?
|
|
97
|
-
|
|
98
|
-
show_card(**card)
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
lambda do |job, fields|
|
|
102
|
-
ContextCandidateChoice.after_judged(job: job, fields: fields, settings: settings, project_root: project_root,
|
|
103
|
-
state_dir: state_dir, log: log, show_card: show)
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
private :context_choice
|
|
107
|
-
|
|
108
|
-
# The cards #context_choice held while a turn ran (#run_turn's ensure,
|
|
109
|
-
# after the turn flag is released).
|
|
110
|
-
def show_context_cards_after_turn
|
|
111
|
-
cards = @context_cards_mutex.synchronize { @context_cards_after_turn.slice!(0..) }
|
|
112
|
-
cards.each { |card| show_card(**card) }
|
|
113
|
-
rescue StandardError => e
|
|
114
|
-
ContextJudge.warn(e)
|
|
115
|
-
end
|
|
116
|
-
private :show_context_cards_after_turn
|
|
117
|
-
|
|
118
|
-
# A delegate child's parent's sources, by title (else name): the
|
|
119
|
-
# judge's started_from. nil for any other session (a chi send --new
|
|
120
|
-
# session has no parent; a fork isn't a delegate).
|
|
121
|
-
def context_parent_attached(session)
|
|
122
|
-
return nil unless session.delegate? && session.parent_id
|
|
123
28
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
Log.warn(:context, "context_parent_attached_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
133
|
-
[]
|
|
29
|
+
# The stage over this Engine's side asks (built in
|
|
30
|
+
# #build_tools_and_plugins, before the plugins load: one may propose a
|
|
31
|
+
# candidate as it loads). Its two callables are the asks thread's late
|
|
32
|
+
# reads: the session the Engine is on and where a card goes, both as
|
|
33
|
+
# an answer comes.
|
|
34
|
+
def build_context_judge
|
|
35
|
+
TurnStages::ContextJudge.new(side_judgments: @side_judgments, host_registry: @host_registry, turn_state: @turn_state,
|
|
36
|
+
show_card: ->(**card) { show_card(**card) }, current_session: -> { @session })
|
|
134
37
|
end
|
|
135
|
-
private :context_parent_attached
|
|
136
38
|
end
|
|
137
39
|
end
|
|
138
40
|
end
|
|
@@ -76,11 +76,13 @@ module Samagotchi
|
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
# Ask the user to approve a call the gate voted `ask` on
|
|
79
|
-
# (GuardrailWiring#request_approval)
|
|
79
|
+
# (GuardrailWiring#request_approval), for the running turn (its
|
|
80
|
+
# author), else the last one. The gate's own asks come with their
|
|
81
|
+
# call's TurnContext instead.
|
|
80
82
|
# @param verdict [Guardrails::Verdict]
|
|
81
83
|
# @return [Guardrails::Verdict]
|
|
82
84
|
def request_approval(verdict)
|
|
83
|
-
@guardrail_wiring.request_approval(verdict)
|
|
85
|
+
@guardrail_wiring.request_approval(verdict, turn_context || last_turn_context)
|
|
84
86
|
end
|
|
85
87
|
end
|
|
86
88
|
end
|