samagotchi 0.148.0 → 0.149.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 +15 -1
- data/docs/architecture.md +1 -1
- data/docs/bundles.md +38 -1
- data/docs/cli.md +17 -0
- data/docs/configuration.md +4 -2
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +2 -1
- data/docs/internals/objectives.md +264 -0
- data/docs/internals/web-frontend.md +4 -1
- data/docs/plugins.md +4 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +42 -0
- data/docs/system-one.md +17 -1
- data/lib/samagotchi/bundles/coordinator/manifest.yml +6 -4
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +9 -0
- data/lib/samagotchi/bundles/decision-card/manifest.yml +13 -0
- data/lib/samagotchi/bundles/decision-card/plugin.rb +280 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +7 -1
- data/lib/samagotchi/context_note.rb +17 -5
- data/lib/samagotchi/engine.rb +113 -6
- data/lib/samagotchi/engine_peers.rb +11 -0
- data/lib/samagotchi/guardrails/pair_boundary.rb +3 -2
- data/lib/samagotchi/llm/chat_loop.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +6 -2
- data/lib/samagotchi/objectives.rb +395 -0
- data/lib/samagotchi/objectives_audit.rb +115 -0
- data/lib/samagotchi/objectives_check.rb +335 -0
- data/lib/samagotchi/objectives_command.rb +192 -0
- data/lib/samagotchi/plugin/api.rb +2 -1
- data/lib/samagotchi/plugin/context.rb +21 -1
- data/lib/samagotchi/session_commands.rb +15 -0
- data/lib/samagotchi/session_continue.rb +3 -0
- data/lib/samagotchi/session_inbox.rb +14 -6
- data/lib/samagotchi/session_manager.rb +1 -1
- data/lib/samagotchi/steer.rb +24 -4
- data/lib/samagotchi/testing/boundary.rb +1 -0
- data/lib/samagotchi/testing/host.rb +1 -0
- data/lib/samagotchi/tool_activity.rb +17 -0
- data/lib/samagotchi/tool_declarations.rb +34 -0
- data/lib/samagotchi/tools/builtin_calls.rb +2 -1
- data/lib/samagotchi/tools/builtins.rb +4 -1
- data/lib/samagotchi/tools/objectives_tool.rb +75 -0
- data/lib/samagotchi/tools/peers.rb +3 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/message_display.rb +9 -0
- data/lib/samagotchi/web/participant_view.rb +1 -1
- data/lib/samagotchi/web/public/app.js +10 -0
- data/lib/samagotchi/web/public/cards_place.js +20 -4
- data/lib/samagotchi/web/public/commands.js +4 -1
- data/lib/samagotchi/web/public/index.html +2 -0
- data/lib/samagotchi/web/public/objectives.css +80 -0
- data/lib/samagotchi/web/public/objectives_entry.js +22 -0
- data/lib/samagotchi/web/public/objectives_model.js +100 -0
- data/lib/samagotchi/web/public/objectives_moves.js +145 -0
- data/lib/samagotchi/web/public/objectives_strip.js +242 -0
- data/lib/samagotchi/web/public/objectives_wiring.js +62 -0
- data/lib/samagotchi/web/public/stream_handlers.js +6 -1
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/session_show.rb +12 -1
- data/lib/samagotchi/worker.rb +3 -0
- data/lib/samagotchi/worker_inbound.rb +16 -8
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c93a3995e0d6192e52b21e25127bedc26e2442b9b45002ed6d3b758fc214c837
|
|
4
|
+
data.tar.gz: 303ef3878f850b90a9f506fb8a09c710a25e18aca6836e8aa59ee27498258146
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da19432429657d32a29891f5ac57d2a987d9f19fb7b8c027f7b8038660e51b76bc4a066331e2ad3a49fe62747b7f8dd85b5cd8f9b59d47282d1ca9b4ab7aaa74
|
|
7
|
+
data.tar.gz: 33d8c08e66bf4eac732fabb7425a1ca00f3fabb8570418d5887c46db9ce0980466969437590908238861d67a267d139c9916e668bc546babf869cae3cc01f162
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and commands may change between minor versions. How releases are made:
|
|
|
7
7
|
[docs/releasing.md](docs/releasing.md).
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.149.0] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `/objectives` gives a session a short list of what it is to achieve (`set A | B`, `add`, `done`, `drop`), kept in the conversation as a note; a continue carries it, and a session with no list gets no note and no prompt line. A first version to try. [docs/sessions.md](docs/sessions.md#objectives)
|
|
15
|
+
- The `objectives` tool lets the model record a move on an objective (a line, optional evidence): information only, it never changes the list, and `/objectives` shows each objective's latest. Declared on `full` and the coordinator toolkit, so a `full` request carries its schema with or without a list; other kits name it in the not-loaded line. The coordinator bundle's skill says a move or a `done` is never approval to merge. [docs/sessions.md](docs/sessions.md#objectives)
|
|
16
|
+
- The `decision-card` bundle (in the `dev` profile): in a session with objectives, a long answer ending in a question gets a card with a short summary and the question, which you answer in the composer. One side request per such answer; a session with no list makes none, and nothing enters the conversation. `chi bundle uninstall decision-card` takes it away. Plugins get `ctx.objectives`. [docs/bundles.md](docs/bundles.md#the-decision-card-bundle)
|
|
17
|
+
- The web shows a session's objectives in the conversation: a strip above it with the current list, each list note drawn as pills or a line of what you changed, and the model's moves as chips above an answer. A tap edits the list with no message or turn (`/objectives edit ID TEXT` is new, and the terminals keep the command), Not yet tells the agent in a message, and on a phone it is one line and a bottom sheet. The `objectives` tool gains `propose`: the model suggests a change and you apply it from a card; it never changes the list by itself. `decision-card`'s `/short` gives a long answer's short version on demand. A session with no list shows nothing new. The tool's schema changed, so the prompt cache misses once. [docs/sessions.md](docs/sessions.md#objectives)
|
|
18
|
+
- Check this (`/objectives check O2`, or the button in the web's list): on request, one side request reads an objective's latest move against what the session's tools returned and people said, and reports a verdict with reasons in a card. Information only: no status changes and nothing enters the conversation. `objectives.check_model` picks a cheaper model, whose host then gets up to 24,000 characters of tool output, known secret shapes redacted.
|
|
19
|
+
- `objectives.system_one: audit` (off by default) saves a System 1 yes/no per active objective on incoming messages and finished answers, for `chi judgments`; nothing acts on it. Its text goes to the System 1 host.
|
|
20
|
+
|
|
21
|
+
Update with `chi update` (coordinator 0.8.0, decision-card 0.2.0, dev profile 0.7.5). `chi update` installs decision-card for anyone on the dev profile; it does nothing in a session with no objectives.
|
|
22
|
+
|
|
10
23
|
## [0.148.0] - 2026-11-04
|
|
11
24
|
|
|
12
25
|
### Added
|
|
@@ -5611,7 +5624,8 @@ and long-lived sessions.
|
|
|
5611
5624
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
5612
5625
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
5613
5626
|
|
|
5614
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
5627
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.149.0...HEAD
|
|
5628
|
+
[0.149.0]: https://github.com/dm1try/samagotchi/compare/v0.148.0...v0.149.0
|
|
5615
5629
|
[0.148.0]: https://github.com/dm1try/samagotchi/compare/v0.147.0...v0.148.0
|
|
5616
5630
|
[0.147.0]: https://github.com/dm1try/samagotchi/compare/v0.146.0...v0.147.0
|
|
5617
5631
|
[0.146.0]: https://github.com/dm1try/samagotchi/compare/v0.145.0...v0.146.0
|
data/docs/architecture.md
CHANGED
|
@@ -101,7 +101,7 @@ terminal renders the events it gets over the Bridge.
|
|
|
101
101
|
| UI | `Samagotchi::TerminalUI` | REPL (Reline), rendering, REPL commands. Delegates all core work to an `Engine`. |
|
|
102
102
|
| Model loops | `KernelLoop` (via `LLM::NativeBackend`), `LLM::ChatLoop` | The model↔tool loop: raw prompt, or a chat API (OpenAI chat, Anthropic Messages), chosen per host (see below). |
|
|
103
103
|
| Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, Anthropic's Messages API, and the HTTP they share. |
|
|
104
|
-
| Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, forget_outputs, find_tools/call_tool (the tool index), plus runtime/output-guardrails. |
|
|
104
|
+
| Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, objectives, forget_outputs, find_tools/call_tool (the tool index), plus runtime/output-guardrails. |
|
|
105
105
|
| Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way merge), build, uninstall; the install record under `.bundles/<name>/` that the hook, guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
|
|
106
106
|
| Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` forks/spawns one worker process per session; its `Worker` builds the `Engine` and `Bridge` (no terminal rendering) and runs queued turns. The spawned process (own process group) forks the worker, which `setsid`s and is reparented to init once its starter exits (it waits until the worker holds `owner.lock`, so the spawner's `Process.detach` waiter tells a starting worker): nothing that walks the spawner's process tree reaches it (`SAMAGOTCHI_WORKER_DETACH=1`, `detach_from_spawner`). The lock's pid is the worker's; `spawn`'s `child_pid` is its starter's. |
|
|
107
107
|
| Web | `Samagotchi::Web::App`, `Samagotchi::Web::Server`, `Samagotchi::Web::SessionHub` | Rack+WEBrick on one port, `127.0.0.1:4567` (index.html + `/api/*` + SSE); with `web.host: lan` also on this machine's LAN address, behind an access token (`Web::Lan`, `Web::Token`). The hub is chi web's projection of the session list, pushed to every tab over `GET /api/events`. |
|
data/docs/bundles.md
CHANGED
|
@@ -858,7 +858,7 @@ by themselves there.
|
|
|
858
858
|
session: the coordinator session picks the change up from the handoff,
|
|
859
859
|
and merges as it always does. Experimental: it may go if it doesn't earn its place.
|
|
860
860
|
- **The coordinator toolkit.** The bundle declares `coordinator`: `lean`
|
|
861
|
-
plus `delegate`, `delegate_result` and `
|
|
861
|
+
plus `delegate`, `delegate_result`, `list_sessions` and `objectives` (the skill records moves with it)
|
|
862
862
|
([Toolkits](configuration.md#toolkits)). `/coordinate` puts a fresh session
|
|
863
863
|
on it before it asks the model (`ctx.use_toolkit`). When chi refuses (a reply
|
|
864
864
|
or a turn already there) and the session's toolkit has no `delegate`, the
|
|
@@ -1240,3 +1240,40 @@ for one model family, loaded into the prompt only for the models a note's
|
|
|
1240
1240
|
through upgrades; other notes stack with the bundle's
|
|
1241
1241
|
([Model notes](memory.md#model-notes)).
|
|
1242
1242
|
|
|
1243
|
+
## The decision-card bundle
|
|
1244
|
+
|
|
1245
|
+
`chi bundle install decision-card` (or the `dev` profile) installs the bundle shipped with chi. It is a try-out, written
|
|
1246
|
+
only against this API (`lib/samagotchi/bundles/decision-card/plugin.rb`). It does nothing in a session with no
|
|
1247
|
+
[objectives](sessions.md#objectives): no request, no card.
|
|
1248
|
+
|
|
1249
|
+
In a session with an active objective, an answer that is long and ends in a question gets a card when the turn is over:
|
|
1250
|
+
a short summary, the question, and the objective it concerns when the model names one. You answer in the composer.
|
|
1251
|
+
The card has no buttons, so it invents no options, and it is no "needs you": no push, no waiting mark.
|
|
1252
|
+
|
|
1253
|
+
- **The gate**: a completed owner's turn (not a stopped one, a participant's or a `--non-interactive` one), at least
|
|
1254
|
+
`min_chars` characters, a question mark that ends a word in the last `tail_chars` of the prose (code is left out). All of
|
|
1255
|
+
it is read before any request.
|
|
1256
|
+
- **The request**: one side answer (`ctx.ask_model`) about the turn's prompt and answer, with the list in its prompt. It
|
|
1257
|
+
counts in `/stats`' side requests under `decision-card`. If the model says the answer asks nothing, or leaves out the
|
|
1258
|
+
summary or the question, there is no card.
|
|
1259
|
+
- **It may be late.** It runs on a thread, so the turn's end doesn't wait, and a one-slot local server answers it after
|
|
1260
|
+
the running request. A new turn cancels it, which frees that slot, and a card still to come is dropped.
|
|
1261
|
+
- **A shown card folds** to a line (`Asked: …`) when your next turn starts (a reminder's, a child's report's or a participant's turn leaves it open): in the web a row in the same place; the terminals
|
|
1262
|
+
print it as an update.
|
|
1263
|
+
- **A failed request** is only logged (`no_card` in the debug log): no card, no notice, no push.
|
|
1264
|
+
- **`/short`**: the last answer's short version on demand, in a card, by the same side request. The web's objectives offer it
|
|
1265
|
+
under the newest long answer of a session with a list (Short version), between turns. Typed, it works in any session; during
|
|
1266
|
+
a turn it refuses, since that turn's answer isn't there yet.
|
|
1267
|
+
- **Nothing else changes**: nothing enters the conversation, so the next request is byte for byte what it would be
|
|
1268
|
+
without the bundle. The card lives in the session's `cards.json`, as any card. `chi bundle uninstall decision-card` takes
|
|
1269
|
+
it all away (a member of a profile you uninstall stays out); a session you have open needs `chi sessions restart ID`.
|
|
1270
|
+
|
|
1271
|
+
```yaml
|
|
1272
|
+
bundles:
|
|
1273
|
+
decision-card:
|
|
1274
|
+
min_chars: 1200 # how long an answer is long
|
|
1275
|
+
tail_chars: 300 # how far from the end the question may be
|
|
1276
|
+
model: null # a cheap configured model ("host:id" or an alias); null is the session's own
|
|
1277
|
+
timeout: 120 # seconds the side request may take (a number)
|
|
1278
|
+
max_tokens: 300 # the side answer's limit
|
|
1279
|
+
```
|
data/docs/cli.md
CHANGED
|
@@ -867,6 +867,23 @@ mid-turn too, and the lists, cards, top bar and tab show it in place of the firs
|
|
|
867
867
|
message; a continue's next link keeps it. In the web, `✎` next to the session's
|
|
868
868
|
title (its strip card, or the top bar) does it. See [Session titles](sessions.md#session-titles).
|
|
869
869
|
|
|
870
|
+
<a id="session-objectives"></a>
|
|
871
|
+
`/objectives` shows what the session is to achieve, each objective's latest move under it (`last move: …`, recorded by
|
|
872
|
+
the model's `objectives` tool); [Objectives](sessions.md#objectives) has what that does.
|
|
873
|
+
- `/objectives set Two coffees | Stops before checkout (done when: no order is placed)`: a new list. Parts are split by
|
|
874
|
+
` | ` or by lines; a part may end `(done when: …)`. It replaces the list, and ids go on from the old one's.
|
|
875
|
+
- `/objectives add TEXT`: one more (several with ` | `).
|
|
876
|
+
- `/objectives edit O1 TEXT`: a new text, the rest of the line whole (it may end `(done when: …)`); id and status stay.
|
|
877
|
+
- `/objectives done O1 [O2…]`, `/objectives drop O1 [O2…]`: mark items. An id it doesn't have refuses the whole line, and
|
|
878
|
+
so does `done` on a dropped item. The model is shown a done item as `O1 [done] …` and no dropped one.
|
|
879
|
+
- `/objectives undo O1 [O2…]`: back to active, after a mistaken `done` or `drop`.
|
|
880
|
+
|
|
881
|
+
- `/objectives check O1`: an independent read of O1's latest move, one side request; the verdict comes as a card and as
|
|
882
|
+
`check: …` under the move. Information only: it edits nothing, and it is refused for an objective with no move or a dropped one, and during a participant's turn. See [Objectives](sessions.md#objectives).
|
|
883
|
+
|
|
884
|
+
All of them run mid-turn too. An edit answers with the new list at once and goes into the conversation between turns.
|
|
885
|
+
Refused in a chi REPL's session (`--no-shared`), as `chi note` is; `/objectives` alone still answers there.
|
|
886
|
+
|
|
870
887
|
A `/model`, `/llm-context`, `/thinking`, `/reasoning-history` or `/toolkit` change that restarts the prompt cache
|
|
871
888
|
asks first: the reply says what it costs, changes nothing, and names the line
|
|
872
889
|
that applies it (`/thinking low --yes`, `/model qwen --default --yes`). `--yes` (or `-y`) applies it at once.
|
data/docs/configuration.md
CHANGED
|
@@ -166,7 +166,7 @@ or `chi web` restart.
|
|
|
166
166
|
| `toolkit.kits`, `execute.description`, `llm_context.policy` | Worker start | These are held by the engine's toolkit or tool registry. |
|
|
167
167
|
| `toolkit.suggest.mode`, `toolkit.suggest.threshold`, `toolkit.suggest.defer_turns`, `toolkit.suggest.defer_tokens` | Worker start | Suggestion settings are config-only and are resolved when the worker runs its audit or on-mode ask. |
|
|
168
168
|
| `skip_agents_md` | Prompt rebuild | A running engine caches its built prompt; a new worker or `/model` rebuild uses the changed value. |
|
|
169
|
-
| `context.*` except `context.every_seconds`, `llm_context.*` except `llm_context.policy`, `steer.cut_after`, `memory.index_warn_tokens`, `memory.entry_warn_tokens`, `status.line`, `web.participants.*` | Next matching turn, display, poll or write | Readers resolve these settings when they perform that operation. Session-specific LLM context and thinking values remain saved in the session. A participant's grant keeps the expiry and cap it was issued with; `web.participants.*` is what the next invite gets, and the cap and queue of a grant from before caps. |
|
|
169
|
+
| `context.*` except `context.every_seconds`, `objectives.*`, `llm_context.*` except `llm_context.policy`, `steer.cut_after`, `memory.index_warn_tokens`, `memory.entry_warn_tokens`, `status.line`, `web.participants.*` | Next matching turn, display, poll or write | Readers resolve these settings when they perform that operation. Session-specific LLM context and thinking values remain saved in the session. A participant's grant keeps the expiry and cap it was issued with; `web.participants.*` is what the next invite gets, and the cap and queue of a grant from before caps. |
|
|
170
170
|
| `context.every_seconds` | Next context poll | The poller checks it for each source that has no explicit interval. |
|
|
171
171
|
| `guardrails.*` | Next tool call or approval | Guardrail rules and disable lists are reloaded when the config file changes; rule settings take effect on that evaluation. `guardrails.parent_answers` is checked when a parent answers a question. |
|
|
172
172
|
| `system_one.host`, `system_one.model`, `system_one.timeout_ms`, `system_one.runs`, `system_one.judgments` | Next tool call or approval | Each ask reads them. |
|
|
@@ -1504,7 +1504,7 @@ toolkit:
|
|
|
1504
1504
|
| `full` | every built-in; the default |
|
|
1505
1505
|
| `lean` | `execute`, `read`, `write`, `edit`, `memory_read`, `memory_write`, `task_create`, `task_wait`, `task_get`, `task_list`, `task_stop`, `ask_user_question`, `web_fetch`, `context_read` |
|
|
1506
1506
|
| `child` | `lean` plus `send_note`, without `memory_write`, `delegate`, `delegate_result` |
|
|
1507
|
-
| `coordinator` | `lean` plus `delegate`, `delegate_result`, `list_sessions` (declared by the coordinator bundle) |
|
|
1507
|
+
| `coordinator` | `lean` plus `delegate`, `delegate_result`, `list_sessions`, `objectives` (declared by the coordinator bundle) |
|
|
1508
1508
|
| `coding` | `child`, with the prompt text `- Be concise in your responses` / `- Show file paths clearly when working with files` (declared by the coordinator bundle, for build and fix children) |
|
|
1509
1509
|
| `review` | `read`, `memory_read` (not `forget_outputs`, even under the `forget` layer): what a [participant's](internals/participants.md#turns) turn may call, whatever the session declares (the review boundary, [guardrails](guardrails.md#a-participants-review-boundary)). Reserved: `toolkit.kits.review` is refused ("review is reserved for participants' turns"), and a bundle can't declare it. A session may run on it too, as any kit |
|
|
1510
1510
|
|
|
@@ -2273,6 +2273,8 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
2273
2273
|
| `context.candidates.threshold` | `0.8` | | The probability at which the judge attaches a candidate in on mode (a judged line's `would: attach`). config.yml only. |
|
|
2274
2274
|
| `context.candidates.offer_threshold` | `0.6` | | The probability from which on mode shows the candidate's card, once (`would: offer`, below `threshold`). config.yml only. |
|
|
2275
2275
|
| `context.candidates.reask_after_turns` | `3` | | Owner prompts after a judged ask before the judge's one re-ask of that candidate. config.yml only. |
|
|
2276
|
+
| `objectives.check_model` | none | | The model [Check this](sessions.md#objectives) asks, its one side request (a host:id, an alias, a host's model id); none: the session's own. **Each Check this sends that model's host up to 24,000 characters of the session's tool output and messages, off the machine with a hosted model even from a local session;** known key shapes and `token=`-style values are redacted first, by pattern only. config.yml only. |
|
|
2277
|
+
| `objectives.system_one` | `off` | | `audit`: System 1 answers a yes/no per active objective on what comes in and goes out, saved as runs and acted on by nothing; see [System 1's saved runs](system-one.md#objectives-in-audit). `off` (the default): no conversation text goes to a System 1 host for this. Its host and model are `system_one.host` / `.model`. config.yml only. |
|
|
2276
2278
|
| `toolkit.default` | `full` | | The toolkit a new session is stored on: the tools its prompts declare (`full`, `lean`, `child`, one of `toolkit.kits` or a bundle's). `chi --toolkit` picks one per session. See "Toolkits". |
|
|
2277
2279
|
| `toolkit.child` | `full` | | A delegate child's toolkit, unless its `delegate` call names one (`toolkit:`). See "Toolkits". |
|
|
2278
2280
|
| `toolkit.suggest.mode` | `off` | | `audit` asks System 1 for a toolkit suggestion without changing the session; `on` waits for it (up to `system_one.timeout_ms`) on an eligible turn and starts the session on a confidently suggested kit (Toolkits, On mode). The System 1 host, model and timeout come from `system_one.*`. config.yml only. |
|
data/docs/guardrails.md
CHANGED
|
@@ -529,7 +529,8 @@ message:
|
|
|
529
529
|
- owner-voiced tools, which act in the owner's name beyond the turn:
|
|
530
530
|
`delegate` and any `delegate_*` tool, `register_reminder`,
|
|
531
531
|
`cancel_reminder`, `send_note`, `forget_outputs`, `task_create`,
|
|
532
|
-
`task_stop`, `find_tools` (it loads tools into the session),
|
|
532
|
+
`task_stop`, `find_tools` (it loads tools into the session), `objectives`
|
|
533
|
+
(a move on the owner's list), and
|
|
533
534
|
`memory_write` (a memory reaches the owner's later sessions); and the tools
|
|
534
535
|
that read the owner's background tasks and their output, `task_wait`,
|
|
535
536
|
`task_get`, `task_list` (a pair turn starts none);
|
data/docs/hooks.md
CHANGED
|
@@ -56,7 +56,7 @@ The plugin class must respond to `#call(event)` — duck-typed, no base class re
|
|
|
56
56
|
|-------|--------------|---------------|
|
|
57
57
|
| `:session_start` | The first turn this chi process runs for the session (so again after `--resume`, a worker that idle-exited and woke, or a restart) | `{ type: :session_start, session_id: "..." }` |
|
|
58
58
|
| `:before_turn` | Before each turn starts | `{ type: :before_turn, session_id: "...", prompt: "..." (nil on a continue), resumed: true \| false (true on a step-limit Continue, which resumes the run before it; false on every other turn, a reminder's or a wake's too), messages: [...] (the history before this turn) }` |
|
|
59
|
-
| `:after_turn` | After a turn completed or was stopped once the model was asked (not after one that failed, was cut short by an interrupt signal (SIGINT), or was stopped before its `:before_turn` hooks) | `{ type: :after_turn, status: "completed" \| "canceled", present: (see [Presenting the answer](#presenting-the-answer-display-only)), refs: (see [Refs on the answer](#refs-on-the-answer-display-only)), messages: [...] (the conversation the turn stored; a cancelled or empty turn ends it with a `kind: turn_note` system message, and a context line is `kind: context`, see [sessions.md](sessions.md#notes-a-turn-leaves-for-the-model)) }` |
|
|
59
|
+
| `:after_turn` | After a turn completed or was stopped once the model was asked (not after one that failed, was cut short by an interrupt signal (SIGINT), or was stopped before its `:before_turn` hooks) | `{ type: :after_turn, status: "completed" \| "canceled", present: (see [Presenting the answer](#presenting-the-answer-display-only)), refs: (see [Refs on the answer](#refs-on-the-answer-display-only)), messages: [...] (the conversation the turn stored; a cancelled or empty turn ends it with a `kind: turn_note` system message, and a context line is `kind: context`, see [sessions.md](sessions.md#notes-a-turn-leaves-for-the-model); on the native loop the last model message still holds its thought block until the next turn), answer: "..." (the answer's text with no thought block; nil for a stopped turn or one that ran out of steps; needs chi newer than 0.148.0) }` |
|
|
60
60
|
| `:before_generation` | Before each LLM API call (both loops), before its `:generation_started`: the hook's time (a question it waits on) isn't the generation's | `{ type: :before_generation, iteration: N }` |
|
|
61
61
|
| `:after_generation` | After LLM returns (both loops); not after a generation a hook cut (`stop_generation`) | `{ type: :after_generation, iteration: N, response: "...", messages: [...] (the conversation as sent) }` |
|
|
62
62
|
| `:generation_progress` | While the response streams, in batches (see [Watching the stream](#watching-the-stream)) | `{ type: :generation_progress, iteration: N, thinking: "..." (new since the last fire), text: "..." (new visible text), thinking_chars: N, text_chars: N (this generation so far), elapsed_ms: N, restarted: true (only on the first fire after a dropped stream was asked again) }` |
|
|
@@ -636,6 +636,7 @@ stripped of surrounding whitespace, except file text, an edit's
|
|
|
636
636
|
| `delegate` | `task` | — | `model`, `session`, `cwd`, `toolkit`, `wait`, `timeout` |
|
|
637
637
|
| `delegate_result` | — | — | `session`, `timeout` |
|
|
638
638
|
| `ask_user_question` | `question` | — | `question`, `options`, `header`, `multi_select`, `allow_freeform` |
|
|
639
|
+
| `objectives` | `note` | — | `action`, `id`, `note`, `evidence`, `change`, `text` |
|
|
639
640
|
| `forget_outputs` | `note` | — | `ids`, `note`, `keep`, `restore` |
|
|
640
641
|
|
|
641
642
|
A `write`'s `content:` is the file text; an `edit` carries `old_text:` and
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Session objectives
|
|
2
|
+
|
|
3
|
+
A session's objectives are a short list the user types: what this session is to achieve. The first version is a
|
|
4
|
+
try-out, so each piece can come out alone. There is no new message kind or store: the list is a note in the
|
|
5
|
+
conversation and the command writes a note file. Terminals show the default note bubble; the web draws it
|
|
6
|
+
([The page](#the-page)). For the user's side, see
|
|
7
|
+
[sessions.md](../sessions.md#objectives) and [cli.md](../cli.md).
|
|
8
|
+
|
|
9
|
+
## The object and the entry
|
|
10
|
+
|
|
11
|
+
`Objectives::List` holds `Objectives::Item`s (`id`, `text`, `status` active|done|dropped, optional `done_when`) and the
|
|
12
|
+
number the next id takes. That number only grows, so an id is never reused, not after a drop or a `set`. `done_when`
|
|
13
|
+
is a hint, never enforced.
|
|
14
|
+
|
|
15
|
+
Each edit is a whole new list in one note entry:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
ContextNote.message(note_id:, text: list.text, source: "objectives", header: ContextNote::OBJECTIVES_HEADER,
|
|
19
|
+
objectives: list.to_payload)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `objectives` is one key in `ContextNote::KEYS`, so everything that copies a note field by field keeps it
|
|
23
|
+
(`ChatLoop#plain`). A note is also what `take_initial_prompt` and `TurnFlow#restore` already handle, which is why
|
|
24
|
+
this isn't a new kind.
|
|
25
|
+
- The payload is `{"next" => 3, "items" => [{"id", "text", "status", "done_when"}]}`, in string keys before a save and
|
|
26
|
+
after one (`List#to_payload`). `Objectives.from_payload` reads either style.
|
|
27
|
+
- It is never sent. The wire gets role and content only, and the frame instructs by itself:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
[OBJECTIVES, set by the user. What the user wants this session to achieve; replaces any earlier list. Keep work on it. Say when something falls outside it. Don't change it yourself.]
|
|
31
|
+
O1 [done] Two coffees, open the cart
|
|
32
|
+
O2 Stops before checkout (done when: no order is placed)
|
|
33
|
+
[END NOTE]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A dropped item is left out of the text, and a list with nothing left says `(no objectives now)` so that it still
|
|
37
|
+
replaces the one before it. The frame has no clock, and there are no system prompt lines about it, so a session with
|
|
38
|
+
no list adds no note and no prompt line. It still carries the tool's bytes ([Moves](#moves)).
|
|
39
|
+
|
|
40
|
+
A note body isn't escaped when it is saved, and a system-role note reaches the model as it stands, so outside text
|
|
41
|
+
could carry a line that looks exactly like this frame. On the wire, `Steer.wire_text` leads such a line with `(quoted) `
|
|
42
|
+
(`Steer::FORGED_OBJECTIVES`). That covers every entry with a kind, on both loops: a context note, a peer's `send_note`,
|
|
43
|
+
`chi note`, a delegate report, a steer. A participant's message gets the wider `FORGED_OPENINGS`. The real note is
|
|
44
|
+
known by its `objectives` payload key, never by its text. The saved content stays raw, the rewrite is deterministic,
|
|
45
|
+
and text with no such line goes out byte for byte, so the prompt cache holds. The owner's typed prompt has no kind and
|
|
46
|
+
isn't touched. Tool output isn't covered: it goes out as a tool result, not as a system message.
|
|
47
|
+
|
|
48
|
+
A payload whose `next` isn't an Integer or whose `items` isn't an Array is not a list (`from_payload` answers nil), so
|
|
49
|
+
it can't wipe the fold or restart the ids.
|
|
50
|
+
|
|
51
|
+
## The current list
|
|
52
|
+
|
|
53
|
+
`Objectives.build(conversation, pending:)` is a pure fold over the entries, shaped like `WorkingSet.build`: the newest
|
|
54
|
+
list note wins, and a pending payload wins over that, because a file not yet absorbed is newer than anything in the
|
|
55
|
+
conversation. `Objectives.pending_payload(session_dir)` finds the newest objectives file among the notes;
|
|
56
|
+
`Objectives.current` is the two together.
|
|
57
|
+
|
|
58
|
+
`build` also fills `List#moves`, the latest `Objectives::Move` per id ([Moves](#moves)). Moves are never in the frame or
|
|
59
|
+
the payload, so an edit and a continue carry none: a new link starts with no last move.
|
|
60
|
+
|
|
61
|
+
## Editing
|
|
62
|
+
|
|
63
|
+
`/objectives` (`ObjectivesCommand`, registered `anytime`) reads `Objectives.current`. An edit goes out through
|
|
64
|
+
`NoteDelivery.deliver(objectives:)`, which means:
|
|
65
|
+
|
|
66
|
+
- One edit's read and write are one step (`Objectives.with_lock`, a file lock in the session's folder, across threads
|
|
67
|
+
and processes), and the worker's absorb holds it from its claim to the files' delete, so no edit reads between them.
|
|
68
|
+
Each note holds a whole list: without this, two edits that read the same list lost one.
|
|
69
|
+
- `done` refuses a dropped item, and `undo` puts an item back to active.
|
|
70
|
+
- A part with no text of its own refuses the line. An empty done-when is none, and of two done-when groups the last
|
|
71
|
+
counts. A leading `[done]`, `[dropped]` or `[active]` is stripped, so a text can't pass for a status. Any whitespace
|
|
72
|
+
collapses, Unicode line separators too, so each item stays one line of the frame.
|
|
73
|
+
- It never writes session.json and runs on any thread: it only adds a file to `notes/`.
|
|
74
|
+
- It is refused in a session a chi REPL owns (`--no-shared`), with the reason `chi note` gives
|
|
75
|
+
(`NoteDelivery::REPL_REFUSAL`). A show still answers there.
|
|
76
|
+
- A second edit stacks on an unabsorbed first, since the current list reads the pending file.
|
|
77
|
+
- `WorkerInbound#absorb_notes` adds only the newest list in a batch and deletes the others, so edits made during a
|
|
78
|
+
turn cost one entry. It runs between turns: an idle edit lands at the worker's next pass (the fallback tick, or just
|
|
79
|
+
before the next turn), and a mid-turn edit waits for the turn's end. A long coordinator turn is the known gap.
|
|
80
|
+
|
|
81
|
+
The attached TUI and the web composer send the line like any session command (the registry's listing tells them it
|
|
82
|
+
runs mid-turn too). So does `chi send -m "/objectives …"`. A participant's command is refused as any other is.
|
|
83
|
+
|
|
84
|
+
## Moves
|
|
85
|
+
|
|
86
|
+
The `objectives` tool (`Tools::ObjectivesTool`, schema last in `TOOL_SCHEMAS`) has one action, `move`: `id`, `note`,
|
|
87
|
+
optional `evidence`. A call with no `action` is a move too. It is information only: nothing is written, and the call in
|
|
88
|
+
history is the record. A handler can't append an entry mid-turn safely, and a system entry would split a call from its
|
|
89
|
+
response, so there is no new entry kind. `Objectives.moves_in` reads the calls back with
|
|
90
|
+
`LLMContextStale.paired_runs`, as `WorkingSet` does: both hosts' forms, a `call_tool` of it as it ran. A run whose output
|
|
91
|
+
counts only if its output is the tool's answer that it recorded that id (`ObjectivesTool.recorded?`): a call with no list,
|
|
92
|
+
a refusal, a denial or an interrupt recorded nothing. A rollback that drops the turn drops its moves.
|
|
93
|
+
|
|
94
|
+
Mid-turn, `session.messages` doesn't hold the running turn. `/objectives` adds the loop's own conversation
|
|
95
|
+
(`Engine#live_conversation`, a `dup` read on the command's thread), because `Engine#plugin_messages`' running part (the
|
|
96
|
+
Bridge's `TurnAccumulator#current_messages`) leaves tool calls out. A move shows once its result is in.
|
|
97
|
+
|
|
98
|
+
- **The answer** says it recorded a move, that the list is unchanged and that the user edits it with `/objectives`; so
|
|
99
|
+
does the schema's description. Other actions get an `Error:` with the same sentence. A small model told to edit the
|
|
100
|
+
list has nothing to call, and this is the only place it is told so: no stable-prompt line.
|
|
101
|
+
- **No list**: `There are no objectives here…`, not an error, for a delegate (its fold is empty), a session with no list
|
|
102
|
+
and a kernel without a session (`Tools::Peers#objectives` is nil; `Engine::PeerView#objectives` reads the session).
|
|
103
|
+
- **Refused**: an unknown id, a dropped objective, an empty note. A `done` objective takes a move.
|
|
104
|
+
- **Declared**: on `full` (`all_core`) and on the coordinator kit (its bundle adds it); named in the not-loaded line
|
|
105
|
+
elsewhere, and so counted in `find_tools`' chi line, and among the deferred tools on a host that loads tools.
|
|
106
|
+
- **Cost**: one extra generation when the move is the step's only call, none when it comes in a batch with another call.
|
|
107
|
+
- **Guardrails**: no rule names it, so it passes with no ask, in `strict` too. `ReviewBoundary` denies it as anything
|
|
108
|
+
outside its kit. `PairBoundary` denies it as owner-voiced: a move stays in the owner's history and reads as the
|
|
109
|
+
session's own.
|
|
110
|
+
- **The row**: `recording a move` in the TUI's line with `id=`, `note=`, `evidence=`; the web's row is `objectives O2 note · evidence`
|
|
111
|
+
(`ToolActivity#tool_title`). No UI of its own.
|
|
112
|
+
|
|
113
|
+
`serves` on `ask_user_question` waits until moves read well. The model can't edit the list.
|
|
114
|
+
|
|
115
|
+
## Proposals
|
|
116
|
+
|
|
117
|
+
`action: propose` (`change` add, reword, done or drop; `id`; `text`; `note`, why) checks the change against the list
|
|
118
|
+
(`Objectives::Proposal#refusal`) and hands it to `Engine#propose_objective`, which keeps it until the turn is over,
|
|
119
|
+
as `#context_choice`'s cards are kept: shown in `run_turn`'s ensure, it is a between-turns card (no `in_turn`), so it
|
|
120
|
+
writes no `pending_card.json`, the stage doesn't say it waits, and it keeps its buttons over a restart (`CardStore` takes
|
|
121
|
+
them only from a card shown in a turn). The same change twice in a turn is one card. A P3 finding stands: a card action
|
|
122
|
+
can't carry a plain reply, so an action here is a real edit.
|
|
123
|
+
|
|
124
|
+
Apply is `/objectives --card ID done O2`: the proposal's own line (`Proposal#line`) behind a prefix the command reads
|
|
125
|
+
(`ObjectivesCommand::CARD`), so it goes through the one route, with its refusals. On success the card with that id is
|
|
126
|
+
replaced by a one-line notice. Dismiss (`--card ID dismiss`) only folds it. Either is recorded under the lock
|
|
127
|
+
(`objectives_cards.json` in the session's folder), so a repeat, from a stale tab or a second tap, applies nothing: it
|
|
128
|
+
says the proposal was already applied and folds the card again. A reword carries what it was proposed against
|
|
129
|
+
(`--was`, `Objectives.fingerprint`) and is refused when the item has changed since. It keeps the item's done-when unless its text
|
|
130
|
+
brings its own. The page keeps a proposal card's buttons off until the folded card comes or the action fails
|
|
131
|
+
(`cards_place.js actionFailed`). A turn leaves at most `PROPOSALS_PER_TURN` (3) cards; past that the tool says so. A refused edit leaves the card, and its
|
|
132
|
+
`command_ran` shows the refusal. Nothing is recorded as a move (`recorded?` is positive); the call stays in history as
|
|
133
|
+
any call. An add's text can't split on ` | `. The body names the line, for a terminal, which shows no button.
|
|
134
|
+
|
|
135
|
+
## The page
|
|
136
|
+
|
|
137
|
+
`Objectives.view` (`List#to_view`) is what the page draws: the items, each with its last move, and `pending`, whether
|
|
138
|
+
the newest list is a file still waiting. The session read (`objectives`, no key with no list) and the live
|
|
139
|
+
`:objectives_updated` carry it. `Engine#announce_objectives` emits when the view differs from the last: after an edit,
|
|
140
|
+
once a list note is in (`add_context_note`), at a turn's end (its moves) and after a rollback between turns. A session
|
|
141
|
+
with no list emits nothing. Between two tabs that is all it takes. A participant gets neither: the event is in
|
|
142
|
+
`ParticipantView::DROPPED`.
|
|
143
|
+
|
|
144
|
+
Idle, an edit lands at once: the worker's anytime path wakes the loop (`@waker.wake`, one extra pass for any anytime
|
|
145
|
+
command), which absorbs the file. It no longer waits for the fallback tick.
|
|
146
|
+
|
|
147
|
+
- **The wiring** (`objectives_wiring.js`): the routes the strip and the moves post on, each following a woken worker
|
|
148
|
+
only while the page still shows that session.
|
|
149
|
+
- **The strip** (`objectives_strip.js`, `#objectivesStrip`; pure parts in `objectives_model.js`, CSS in
|
|
150
|
+
`objectives.css`). A factory with its own closure (the list; never `store.current`), seeded by `renderSessionView` and reset
|
|
151
|
+
by `forgetSession`. Its edit list is a `chip_popover.js` shell, so a phone gets the bottom sheet and the focus rules.
|
|
152
|
+
Every edit is a line on the command route with `card: true`: no echo, and `commandView` hides a good `/objectives`
|
|
153
|
+
answer that came with that mark, so a tap leaves no bubble, a refusal does. The strip redraws from the event, never
|
|
154
|
+
optimistically.
|
|
155
|
+
- **The entry** (`objectives_entry.js`, from `turn_html.js noteBubbleTemplate`): `Objectives.entry` puts the note's items
|
|
156
|
+
and, against the list before it, `changes` (`Objectives.changes`, display only) on the history item and on
|
|
157
|
+
`context_added`. A first list is pills; a later one a line, `You marked O1 done`. The raw text stays the tooltip.
|
|
158
|
+
- **Moves** (`objectives_moves.js`): a tool row's id is a chip (`moveRowText`), and a row that recorded a move carries
|
|
159
|
+
`data-move-id`. One observer on `#history` reads those back into a line after each finished `.turn-work`, so a reload,
|
|
160
|
+
a live end and a hand-off all draw it. The newest line's Not yet is `objectivesStrip.notYet`. A decision card's
|
|
161
|
+
`Concerns O2` gets the chip there too.
|
|
162
|
+
- **Not yet** is `POST /turn` with a client id of its own (`<tab>:objectives`), so the stream draws its bubble in every
|
|
163
|
+
tab, this one too: the page's own echo needs the composer. It steers a running turn or starts a turn, as a typed line.
|
|
164
|
+
- **Short version**: the same observer offers `/short` (decision-card) under the newest long answer (`foldable`), only with a list
|
|
165
|
+
and with the command in the session's listing, and not during a turn: `/short` itself refuses then, since a running
|
|
166
|
+
turn's "last answer" is interim text. A request that fails, or a worker that leaves with one still out (the bundle's service
|
|
167
|
+
stops after chi's 3 s wait for a command), ends the card saying so; a worker killed outright leaves its one-line `…` row.
|
|
168
|
+
|
|
169
|
+
## Continue, rollback, fork
|
|
170
|
+
|
|
171
|
+
- **Continue**: `Objectives.carry`, in `SessionContinue`'s `before_spawn`, writes a second note file in the new link,
|
|
172
|
+
after the carry note and any moved context's note, so the list reads as the latest word. It is not in
|
|
173
|
+
`SessionChain.note_text` (capped; the last answer gets what the recap leaves). It reads the previous link fresh,
|
|
174
|
+
with its unabsorbed newest file. A failure is only logged (`continue_objectives_failed`), because it runs inside the
|
|
175
|
+
continue's rollback. A review link's list stays behind, as its last answer does: the next link's participants could
|
|
176
|
+
differ.
|
|
177
|
+
- **Rollback**: a note survives `TurnFlow#restore`, and an unabsorbed edit is on disk, untouched.
|
|
178
|
+
- **Fork, delegate**: a plugin fork copies what it is passed. A delegate starts with an empty fold, so the tool answers
|
|
179
|
+
that there are none.
|
|
180
|
+
|
|
181
|
+
## The long-answer card
|
|
182
|
+
|
|
183
|
+
A bundle, not core: [decision-card](../bundles.md#the-decision-card-bundle), removed by uninstalling. It reads the list with
|
|
184
|
+
`ctx.objectives`, the plugin API over `Objectives.current`; `items` and `text` are the bundle's only view of the object.
|
|
185
|
+
|
|
186
|
+
`:after_turn` reads the answer's length and tail from `event[:answer]`, not from the stored message, which on the native
|
|
187
|
+
loop still holds its thought block, and starts a thread only when both pass, so the turn's end
|
|
188
|
+
is held for no request and the web's answer doesn't wait (no `event[:present]`). The thread reads the list and, with an active
|
|
189
|
+
objective, makes one `ctx.ask_model`. A Transcript drops system entries, so the list goes in `prompt:`. The objective a reply
|
|
190
|
+
names is display only: an id the list doesn't have, or has dropped, is left out.
|
|
191
|
+
|
|
192
|
+
Stale is a new turn: `:before_turn` takes the running `Ask` and cancels its request, so a one-slot server is free for that
|
|
193
|
+
turn, and `show` holds the same mutex, so a card shows before that start or not at all. The hook runs inside its turn, so the
|
|
194
|
+
card waits for `ctx.turn_running?` to go false: shown earlier, it gets `in_turn: true` and lands in that turn's collapsed block.
|
|
195
|
+
The request doesn't wait: it goes ahead of the turn-end warm-up, so on a one-slot server the warm-up queues behind it and is the
|
|
196
|
+
last thing in the slot, and the next turn keeps its prefilled prompt. A shown card is replaced by a one-line one with
|
|
197
|
+
the same id only at a turn of the owner's (a prompt, no author): a wake has no prompt, and a child's report isn't an answer. A
|
|
198
|
+
worker that restarts forgets a shown card, which then stays as it is: it has no buttons.
|
|
199
|
+
|
|
200
|
+
A card with no actions writes no `pending_card.json`, so it never reaches `Web::Push::Attention`.
|
|
201
|
+
|
|
202
|
+
## Check this
|
|
203
|
+
|
|
204
|
+
`/objectives check O2` and the list's **Check this** (an objective with a move, the one route) run `ObjectivesCheck.run` on the
|
|
205
|
+
anytime command's own thread: one side request (`Engine#ask_objective_check`, on `ask_side_model`) reads the objective's latest
|
|
206
|
+
move against the session's record, and the answer is a verdict with reasons. Information only: no status changes, it is
|
|
207
|
+
never an approval, no coordinator path reads it, and nothing enters the conversation or the next request.
|
|
208
|
+
|
|
209
|
+
**Why not a `VerifyFresh` child.** The plan said "VerifyFresh-like, fit unchecked". It fits poorly:
|
|
210
|
+
|
|
211
|
+
- `VerifyFresh.start` starts from a saved unclear verdict and its `VerifyAnchor`, a quote of at most 200 characters that
|
|
212
|
+
`verify_anchor_source_session` holds against the parent's text. A move has neither, and a faked one would land in `session.verdicts`.
|
|
213
|
+
- A child is an agent on the session's model with the owner's tools: an unknown number of requests, approvals waiting
|
|
214
|
+
on the child, a place in `session.max_children` and in the session lists, and its spend outside the parent's line.
|
|
215
|
+
Keeping it read-only and out of the delegate paths (`ChildRing`, `ChildrenStatus`) needs a new session field.
|
|
216
|
+
|
|
217
|
+
The side request costs exactly one request, shows in the session's side line (kind `objectives`) and comes out with one
|
|
218
|
+
file. What it gives up: **it can't open a file or run anything.** It reads what the session saw, so a claim that nothing in
|
|
219
|
+
the record settles is `unclear`, and a file changed since isn't seen. If that turns out too weak, a read-only child is the
|
|
220
|
+
next step, and the file and view below can stay.
|
|
221
|
+
|
|
222
|
+
**Independence.** The request (`ObjectivesCheck.request`) has its own system text and no tools. Its record is what people and
|
|
223
|
+
notes said and what tools returned (`.record`, each entry cut at 1,500 characters, the newest 24,000 kept). It never holds the
|
|
224
|
+
agent's prose, the system prompt, the list's notes, a continue's carry note, which holds the agent's last answer
|
|
225
|
+
(`Objectives.outside_note?`, the rule the audit shares), or the objectives tool's calls, since a move isn't evidence for itself.
|
|
226
|
+
Note and evidence are capped at 1,000 characters each. Mid-turn it reads the loop's conversation (`Engine#live_conversation`), the running turn's runs too.
|
|
227
|
+
|
|
228
|
+
**Its frame.** The claim and the record sit between `<record-RANDOM>` tags made per request, so tool output printing a closing tag
|
|
229
|
+
stays inside; the request says that what's inside is data and its instructions aren't to be followed. No model is needed for
|
|
230
|
+
the frame to hold; a model may still be swayed by text inside it, which is why a verdict is information only.
|
|
231
|
+
|
|
232
|
+
**Its host.** The model is the session's, or `objectives.check_model`, whose host may be another one. The whole user text goes through
|
|
233
|
+
`Web::ParticipantView::Redact.text`, chi's participant redaction: known key shapes and `token=`-style values become `[redacted]`, and
|
|
234
|
+
the home folder `~`. That is by pattern, so a secret of another shape still goes.
|
|
235
|
+
|
|
236
|
+
**State.** `objective_checks.jsonl` beside the session: a `running` line, then one with `verdict` (confirmed, refuted, unclear,
|
|
237
|
+
`no_answer`), `reason`, `text`, `model` and `mark`: a digest of the item's line as sent and the move itself (`Move#ref`, its run's
|
|
238
|
+
id and its model entry's time, with its words). The start is under `Objectives.with_lock`, so a double tap or a second tab answers "already
|
|
239
|
+
being checked" and asks nothing. A running line stops counting once its worker's pid is gone, so a kill, a shutdown or a restart frees
|
|
240
|
+
the next check at once, and past `RUNNING_SECONDS` in any case. `Objectives.view` puts the last check on its item (`check`), only while `mark`
|
|
241
|
+
matches: a reword, a status change or a newer move, in the same words too, offers the check again. A running check carries `until`, because no event says
|
|
242
|
+
its worker died: the strip lets the row go at that time (`armExpiry`), and a reconnect's session read clears it sooner. `announce_objectives` carries the start and the end. A continue doesn't carry it.
|
|
243
|
+
|
|
244
|
+
**Shown as** a card with no actions (`source: objectives`, on the command's thread, so `anytime` and not in a turn's block, and it
|
|
245
|
+
writes no `pending_card.json`, so no push), the list's row, and `check:` under the move in `/objectives`. A model that fails is
|
|
246
|
+
`no_answer`: logged (`objectives_check_failed`), an info card saying so, and the next tap may try again.
|
|
247
|
+
|
|
248
|
+
**Limits.** Refused during a participant's turn, whose text stays out of a side request; one ending during their turn is still the owner's (`Engine#side_author`). A participant can't reach it: the
|
|
249
|
+
command route is the owner's, and `card` and `objectives_updated` are in `ParticipantView::DROPPED`. A move made in the running
|
|
250
|
+
turn isn't on the page until the turn's end (`announce_objectives`), so its tap waits for that; the typed line works at once.
|
|
251
|
+
|
|
252
|
+
## System 1 in audit
|
|
253
|
+
|
|
254
|
+
`ObjectivesAudit`, off unless `objectives.system_one: audit`. See [system-one.md](../system-one.md#objectives-in-audit).
|
|
255
|
+
|
|
256
|
+
## Not in this version
|
|
257
|
+
|
|
258
|
+
Stable-prompt lines aren't here, and terminals have no picture: they keep `/objectives`.
|
|
259
|
+
|
|
260
|
+
Taking it out: old notes stay readable framed bubbles and keep being sent, and the key is ignored. Old `objectives` calls
|
|
261
|
+
stay as history; with a kit no longer declaring the tool, they are ordinary saved calls. The card's bundle uninstalls. The
|
|
262
|
+
page's pieces are the `objectives_*.js` modules and `objectives.css`, wired in a few lines of `app.js`; without them a list note
|
|
263
|
+
falls back to the default bubble once `noteBubbleTemplate`'s first line goes. Check this is `objectives_check.rb`, its line in the command and
|
|
264
|
+
the `check` key; the audit is `objectives_audit.rb` and three calls in `Engine`. Old `objective_checks.jsonl` files and saved runs are then read by nothing.
|
|
@@ -23,7 +23,7 @@ page. A factory dep a new version stops passing is the same hazard the other way
|
|
|
23
23
|
out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
|
|
24
24
|
until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
|
|
25
25
|
`turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
|
|
26
|
-
`session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep)
|
|
26
|
+
`session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js` and the `objectives_*.js` modules are new modules for that reason.
|
|
27
27
|
|
|
28
28
|
## Two pages, two models
|
|
29
29
|
|
|
@@ -54,6 +54,9 @@ change.
|
|
|
54
54
|
| **localStorage** (`storage.js` `readStored`/`writeStored`, guarded) | per-browser choices only | the module that owns the choice | read at start (see Storage keys) |
|
|
55
55
|
| **SSE** | see Streams | the server | owner page: events go into the DOM directly; participant page: an event marks the data stale |
|
|
56
56
|
|
|
57
|
+
The objectives' strip keeps the list in its own closure (`objectives_strip.js`, seeded by `renderSessionView`, reset by
|
|
58
|
+
`forgetSession`, updated by `objectives_updated`): [objectives.md](objectives.md#the-page).
|
|
59
|
+
|
|
57
60
|
The start page's own choices (the model, the ⚙ panel, the 🧰 list) live in `start_page.js`'s closure, never in
|
|
58
61
|
`store.current`. A fresh chain link's picker reads both.
|
|
59
62
|
|
data/docs/plugins.md
CHANGED
|
@@ -850,10 +850,12 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
850
850
|
| `ctx.messages` | the conversation, as a frozen copy, without the system prompt. While a turn runs, a session worker's (attached, web) adds that turn so far: its prompt, the model's text and the lines merged into it (no tool calls or thinking); the REPL's is the conversation before that turn. An entry may carry `at`, when it happened (docs/sessions.md, Message times) |
|
|
851
851
|
| `ctx.messages_partial?` | whether `ctx.messages` leaves out a running turn (the REPL mid-turn), so a plugin can say what its answer is about |
|
|
852
852
|
| `ctx.prompt_files` | the files whose text is in the session's system prompt now: the AGENTS.md chain (the work tree top down to the cwd) and the appended files (`append_system_prompt:`, `--append-system-prompt`); absolute paths, in prompt order, `[]` before the first build |
|
|
853
|
+
| `ctx.objectives` | the session's [objectives](internals/objectives.md), read now, as a frozen value: `empty?`, `items` (each `id`, `text`, `status` `"active"`, `"done"` or `"dropped"`, `done_when`), `find(id)`, `text` (the lines the model is shown) and `moves` (the latest move per id; mid-turn, a worker's leaves the running turn's out). An edit not yet absorbed counts. Empty when the session has none. Use its methods, not its class. Needs chi newer than 0.148.0 |
|
|
853
854
|
| `ctx.notify(text, level: :info, fallback_for: nil)` | one line to the user, labelled by the bundle (`my-bundle> …`). Every UI shows it, during a turn (a tool, a hook) or between turns (a command). `fallback_for: :display` marks a line that repeats the answer's display (`event[:present]`), which a UI that renders the display leaves out (`fallback_for: :editor`: a file the viewer could open from a ref in its editor): see [Hooks](hooks.md#what-a-hook-can-do-the-runtime). The keyword needs chi 0.35.0 (it raises on an older chi) |
|
|
854
855
|
| `ctx.card(title:, body: "", actions: [], level: :info, id: nil)` | a card in every UI, returning its id: see [Cards](#cards) |
|
|
855
856
|
| `ctx.ask_user(question:, options:, header: nil, allow_freeform: false, effects: nil)` | a single-select question through the question flow: `{selected:, freeform:, selected_indices:}`, or nil (no one to ask, cancelled, bad options or effects). `effects:` tags the options a parent agent's answer is held to (label => `:spend`, `:permit`, `:permit_wide` or `:deny`; untagged = `:answer`): tag every option that spends or permits, and never let text alone or a dismissal do either ([hooks.md](hooks.md#what-a-hook-can-do-the-runtime)). It needs a chi newer than 0.85.0 |
|
|
856
857
|
| `ctx.cancelled?` | whether the running turn was cancelled (a long tool should stop) |
|
|
858
|
+
| `ctx.turn_running?` | whether a turn runs now. An `:after_turn` hook runs inside its turn, so a card a thread shows before the turn is over belongs to that turn's block, which the web collapses: wait for this to go false. Needs chi newer than 0.148.0 |
|
|
857
859
|
| `ctx.steer(text)` | put text into the running turn: its own user message at the loop's next boundary, shown as `my-bundle> nudged: …`. The model reads it with a header naming your bundle (`[Steer from the my-bundle plugin, mid-task. …]`); the session keeps the raw text. Returns true when queued, false with no turn running (it never starts one; that is `ctx.sessions`' send). Dropped (logged) if the model answers or the turn ends first. Safe from any thread: an anytime command, a hook (false from `:after_turn`), your own |
|
|
858
860
|
| `ctx.stop_turn(reason)` | stop the running turn after a warn notice with the reason; true when it stopped one now. In a `:before_tool_call` block it also denies that call |
|
|
859
861
|
| `ctx.stop_generation(reason)` | cut the generation that is streaming: the turn goes on and the model is asked again ([hooks.md](hooks.md#watching-the-stream)). Shows nothing: post your own notice. True when it cut one now |
|
|
@@ -1377,8 +1379,8 @@ that calls it, or for a real external bundle that does. No method on the chance
|
|
|
1377
1379
|
`Samagotchi::Tools::Execute.timeout_seconds`.
|
|
1378
1380
|
5. `Samagotchi::Tools::TaskRuntime::TASKS_DIR` and `EXIT_CODE_FILENAME`.
|
|
1379
1381
|
6. `Samagotchi::AtomicFile.write`, `Samagotchi::Log.warn` /
|
|
1380
|
-
`Samagotchi::Log.debug`, `Samagotchi::Config.get
|
|
1381
|
-
`Samagotchi::VERSION
|
|
1382
|
+
`Samagotchi::Log.debug`, `Samagotchi::Config.get`,
|
|
1383
|
+
`Samagotchi::VERSION` and `Samagotchi::CancellationController` (`new`, `cancel!`: what `cancel:` takes).
|
|
1382
1384
|
|
|
1383
1385
|
`Samagotchi::Testing::Boundary::ALLOWED` is the checked copy of this list,
|
|
1384
1386
|
and `Boundary::EXCEPTIONS` the per-bundle list of what each shipped bundle
|
data/docs/releasing.md
CHANGED
|
@@ -13,7 +13,7 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
13
13
|
and `rake release:check` enforce it). The system bundle upgrades itself when
|
|
14
14
|
chi starts.
|
|
15
15
|
- **Every other shipped bundle** (btw, check-in, guardrails, known-names,
|
|
16
|
-
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, and the profiles core and dev) has its own semver in its
|
|
16
|
+
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, decision-card, and the profiles core and dev) has its own semver in its
|
|
17
17
|
`manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
|
|
18
18
|
`chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
|
|
19
19
|
- a change to a bundle's files bumps that bundle's `version:`
|
data/docs/sessions.md
CHANGED
|
@@ -192,6 +192,48 @@ OSA
|
|
|
192
192
|
[ -n "$picked" ] && pbpaste | "$chi" note --source clipboard $(print -r -- "$picked" | cut -f1)
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
+
## Objectives
|
|
196
|
+
|
|
197
|
+
`/objectives set A | B` gives a session a short list of what it is to achieve, and `/objectives` shows it. The list goes
|
|
198
|
+
into the conversation as a note under its own frame (`[OBJECTIVES, set by the user. …]`), which asks the model to keep
|
|
199
|
+
work on it, to say when something falls outside it and not to change it itself. A session with no list behaves as it
|
|
200
|
+
would without the feature; its request only carries the `objectives` tool below (declared on `full`, named in the
|
|
201
|
+
not-loaded line elsewhere). Each item has an id (`O1`, never reused), a status (active, done, dropped) and
|
|
202
|
+
optionally a done-when hint, which nothing enforces.
|
|
203
|
+
|
|
204
|
+
An edit waits for a moment between turns: it lands within seconds in an idle session, and at the turn's end during one.
|
|
205
|
+
Several edits made during a turn go in as one list. A continue copies the list to the next link, as a second note after
|
|
206
|
+
the carry note; a review link doesn't. `!rollback` keeps it. A chi REPL's session (`--no-shared`) refuses an edit, as it
|
|
207
|
+
refuses a note. See [cli.md](cli.md#session-objectives) for the commands and
|
|
208
|
+
[internals/objectives.md](internals/objectives.md) for the mechanism.
|
|
209
|
+
|
|
210
|
+
The model can record a **move** with the `objectives` tool: a line on what it did or found for an objective, with
|
|
211
|
+
optional evidence. A move shows as the tool's row in the turn, and `/objectives` shows each objective's latest one under
|
|
212
|
+
it. It is information only: only you change the list or a status, and it never approves anything. A move costs one
|
|
213
|
+
generation when it's the step's only call. On a kit that doesn't declare the tool, `/toolkit add objectives` declares it
|
|
214
|
+
(the model can also reach it with `call_tool`). A participant's turn is denied it.
|
|
215
|
+
|
|
216
|
+
The model can also **propose** a change with that tool (add, reword, done, drop). You see a card when the turn is over:
|
|
217
|
+
Apply makes the edit, once, Dismiss leaves the list, and ignoring it changes nothing. A turn leaves at most 3. A proposed
|
|
218
|
+
reword is refused when you changed that objective since. The model never changes the list by itself.
|
|
219
|
+
|
|
220
|
+
**Check this** (`/objectives check O2`, or the button in the web's list, for an objective with a move) asks for an independent read of
|
|
221
|
+
the latest move: one side request, on your request only, on the session's model or `objectives.check_model`. It sees the
|
|
222
|
+
claim, what people and notes said and what tools returned, not the agent's prose, and no file: what the record doesn't
|
|
223
|
+
settle comes back `unclear`. **That is up to 24,000 characters of raw tool output, which with a hosted `check_model` leaves the machine on one tap, even from a local session.** chi's redaction takes out known key shapes and `token=`-style values first, by pattern only, so another secret in a tool's output still goes. The verdict (confirmed, refuted, unclear) and its reasons come as a card and under the
|
|
224
|
+
objective. It is information only: it changes no status, approves nothing, and isn't in the conversation or the model's next request.
|
|
225
|
+
A second tap while one runs asks nothing. Its cost is in the session's side line. Mid-turn it runs beside the turn; on a one-slot
|
|
226
|
+
local server it answers after the running request. A participant can't reach it. See
|
|
227
|
+
[internals/objectives.md](internals/objectives.md#check-this).
|
|
228
|
+
|
|
229
|
+
**In the web** a session with a list has a strip above the conversation: its objectives and how many are done (a phone
|
|
230
|
+
shows one line, the one being worked on). A tap opens the list, where you add, mark done, drop, undo and tap a text to
|
|
231
|
+
reword it, with no message and no turn. Mid-turn the strip has the change and says it applies after this turn. Each list
|
|
232
|
+
in the conversation shows as pills, or as one line of what you changed. A turn's moves show as chips above its answer;
|
|
233
|
+
Not yet on one (or in the list, for an objective with a move) sends `Not yet on O2 (…).` as a normal message. With the
|
|
234
|
+
[decision-card](bundles.md#the-decision-card-bundle) bundle, the newest long answer offers its Short version in a card,
|
|
235
|
+
which enters no history. A session with no list shows none of this. A participant doesn't see the list.
|
|
236
|
+
|
|
195
237
|
## Notes a turn leaves for the model
|
|
196
238
|
|
|
197
239
|
A turn that ends without an answer leaves one line for the model to read on its next turn, as a tail
|