samagotchi 0.14.0 → 0.16.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 +77 -1
- data/docs/architecture.md +10 -0
- data/docs/cli.md +3 -2
- data/docs/configuration.md +3 -1
- data/docs/guardrails.md +16 -2
- data/docs/plugins.md +3 -3
- data/docs/sessions.md +7 -2
- data/docs/sub-agent.md +15 -3
- data/lib/samagotchi/answer_display.rb +6 -2
- data/lib/samagotchi/bridge/card_store.rb +16 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +6 -0
- data/lib/samagotchi/bridge.rb +84 -0
- data/lib/samagotchi/bridge_client.rb +33 -6
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/empty_answer_retry.rb +2 -1
- data/lib/samagotchi/engine.rb +58 -17
- data/lib/samagotchi/kernel_loop.rb +16 -5
- data/lib/samagotchi/llm/chat_loop.rb +17 -9
- data/lib/samagotchi/llm/model_result.rb +12 -7
- data/lib/samagotchi/log_subscriber.rb +1 -0
- data/lib/samagotchi/parent_report.rb +8 -0
- data/lib/samagotchi/plugin_session_state.rb +30 -0
- data/lib/samagotchi/question_desk.rb +97 -18
- data/lib/samagotchi/relay.rb +88 -0
- data/lib/samagotchi/relay_desk.rb +73 -0
- data/lib/samagotchi/relay_verifier.rb +113 -0
- data/lib/samagotchi/relay_watcher.rb +46 -0
- data/lib/samagotchi/reply_wait.rb +10 -1
- data/lib/samagotchi/self_report.rb +24 -4
- data/lib/samagotchi/session.rb +15 -2
- data/lib/samagotchi/session_manager.rb +5 -2
- data/lib/samagotchi/session_metrics.rb +18 -1
- data/lib/samagotchi/session_retention.rb +6 -1
- data/lib/samagotchi/sessions_command.rb +10 -4
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +11 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +88 -12
- data/lib/samagotchi/terminal_ui/event_renderer.rb +6 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -0
- data/lib/samagotchi/terminal_ui/question_prompt.rb +48 -2
- data/lib/samagotchi/terminal_ui/question_slot.rb +5 -1
- data/lib/samagotchi/terminal_ui.rb +11 -0
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tools/delegate_relay.rb +216 -0
- data/lib/samagotchi/tools/delegate_wait.rb +72 -6
- data/lib/samagotchi/tools/peers.rb +6 -4
- data/lib/samagotchi/turn_note.rb +30 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +24 -0
- data/lib/samagotchi/web/public/app.js +32 -11
- data/lib/samagotchi/web/public/index.html +9 -0
- data/lib/samagotchi/web/public/notify.js +18 -5
- data/lib/samagotchi/web/public/question_card.js +45 -3
- data/lib/samagotchi/web/public/question_cards.js +49 -1
- data/lib/samagotchi/web/public/sessions_list.js +16 -1
- data/lib/samagotchi/web/public/stage_model.js +8 -3
- data/lib/samagotchi/web/public/stage_view.js +7 -2
- data/lib/samagotchi/web/public/strip.js +8 -1
- data/lib/samagotchi/web/public/timing.js +22 -5
- data/lib/samagotchi/web/public/turn_events.js +9 -0
- data/lib/samagotchi/web/public/turn_view.js +38 -13
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0dd06759f4b1d8b5c2d799aed0a4a57940c34bfd663599fcd1dbbad4f4421c99
|
|
4
|
+
data.tar.gz: 040c8e4d3c4e3aee3f9605fded5676088c21a80b50426696a9cdee4a16f65d7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 418ba106ef5c7f610fd831b93b3441ab1de00910b84a782d37bc3fe617dab833ce35073f4733dc2fe6bdca9d90dba5c7af6fba7b7df29edf60c72c08c99acff9
|
|
7
|
+
data.tar.gz: 8c78e649bbadc097d5cb9b3de7adaac3bcebeda06d04ea1a34f7cbfab531fcaf15e0bce7bbc25480eb28e18c47867cbd0226d57ac3cece9452e7912471aa55b5
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,80 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.16.0] - 2026-10-03
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- A delegated child's guardrail approvals go to the parent session's user (the approval relay): while the parent
|
|
16
|
+
waits (`delegate`, `delegate_result`), the child's approval opens as the parent's own approval, in the parent's web
|
|
17
|
+
page and attached terminal, with the same facts and diff and a line naming the delegate (linked) and its task. The
|
|
18
|
+
user answers once there; the parent's model sees only an outcome line in the tool's result (`approval relayed to
|
|
19
|
+
your user: execute: git push → allowed once`). The child verifies each answer with the parent's own worker before
|
|
20
|
+
taking it, and holds a parent agent's answer to its own `guardrails.parent_approvals`. The child's card stays
|
|
21
|
+
answerable (first answer wins) and says it waits in the parent too; `chi sessions list` shows it as
|
|
22
|
+
`waiting (in parent ab12)` (json `relayed_to`), and the web's bell rings once, for the parent's card. The other
|
|
23
|
+
running children's approvals are relayed during a wait too, one card at a time (`+1 more delegate waiting`), and a
|
|
24
|
+
grandchild's hop by hop. The time a card is open doesn't count against the wait's timeout.
|
|
25
|
+
- For other parents (`chi send --wait`, `chi answer`), an approval's question block also offers to leave it open for
|
|
26
|
+
the user: `chi send --wait --format json ID` waits until they answer it in chi.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- A turn that ends with no answer (the empty-answer retries used up) saves the same shape from both loops: the hidden
|
|
31
|
+
turn note alone. The native loop no longer saves the empty thinking-only reply and a literal `[No response]` (the
|
|
32
|
+
next prompt had two assistant turns in a row), and the chat loop's `(the model returned an empty answer)` is no
|
|
33
|
+
longer the answer text (`chi send --wait` saw it as an answer). The note carries a marker the UIs draw from, never
|
|
34
|
+
sent to the model. Older sessions keep their `[No response]`.
|
|
35
|
+
- A parent agent's answer (`chi answer`, a marked `chi --attach`) no longer brings an archived session back to the
|
|
36
|
+
lists; only a human's does.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- A turn with no answer shows one muted line in the attached TUI and the REPL,
|
|
41
|
+
`no answer: the model returned nothing (after 1 retry)`, like the retry row, and so does `chi --resume` when it was
|
|
42
|
+
the last turn (it showed `[No response]` or an earlier step's text as the answer, or nothing).
|
|
43
|
+
- The web shows a turn with no answer as the same muted notice where the answer would go, before the timing line,
|
|
44
|
+
live and after a reload; the reload keeps the turn's steps (both thinkings, the retry row inside them). Live it
|
|
45
|
+
showed nothing (native hosts) or an answer bubble below the timing line (`api: openai`); a reload showed
|
|
46
|
+
`[No response]` as the answer with one step fewer, or lost the steps and turned the retry row into a bubble of
|
|
47
|
+
its own.
|
|
48
|
+
- Attached `chi -p` from a script exits 1 with `chi: the model gave an empty answer` on stderr when its turn ends with
|
|
49
|
+
no answer, as `chi -p --non-interactive` does; it exited 0 with a blank line (native hosts) or the placeholder text
|
|
50
|
+
(`api: openai`) as the answer. `chi send --wait` reports such a turn as `no_answer` from both kinds of host (an
|
|
51
|
+
`api: openai` host's placeholder was reported as the answer).
|
|
52
|
+
- `chi --resume` and `--attach` give the last exchange's replayed tool rows their durations, as the live rows show
|
|
53
|
+
them (`ok (1.3s)`), from the session's saved tool records; a session from before turn ids keeps them bare.
|
|
54
|
+
- The web stage's status row reads `∅ no answer` (muted, with a warn edge) for a turn that ended with no answer; it
|
|
55
|
+
said `✓ answered`.
|
|
56
|
+
|
|
57
|
+
Update with `chi update` (no bundle changes).
|
|
58
|
+
|
|
59
|
+
## [0.15.0] - 2026-10-02
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- The hidden session strip's pill counts the sessions that wait for you (`▾ 5 sessions · 1 live · 1 waiting`, in the
|
|
64
|
+
warn colour).
|
|
65
|
+
- The all-sessions search finds the sessions that wait for you: `waiting`, `question`, `approval` or `needs you`.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
- `chi --resume ID` (and `--attach`) shows the last exchange's tool calls as the live tool rows
|
|
70
|
+
(`tool> running command (execute command="true"): ok`) between the prompt and the answer, not the prompt and the
|
|
71
|
+
answer alone.
|
|
72
|
+
- A `--no-shared` REPL keeps the session's `cards.json` as a worker does: its turns count, so the cards and notices an
|
|
73
|
+
earlier worker showed stay where they were in the web, and the REPL's own are saved too.
|
|
74
|
+
- A deleted, discarded or retention-pruned session takes its plugins' state with it: a bundle's
|
|
75
|
+
`plugins/<bundle>/sessions/<id>.json` (or `<id>/`) under the state dir, such as check-in's `/checkin` settings, no
|
|
76
|
+
longer stays behind.
|
|
77
|
+
- `chi self`'s context window row shows the current model's configured window: its `models.<key>.window_tokens`, else
|
|
78
|
+
its host's `hosts.<name>.window_tokens`, else `context.window_tokens` or the default, and says which.
|
|
79
|
+
- The web pairs each prompt with its own turn's timing: a failed turn (its prompt went back to you) no longer shifts
|
|
80
|
+
the timing lines and turn numbers of the turns after it. chi saves a turn id on each prompt; prompts saved before
|
|
81
|
+
this still pair by their place.
|
|
82
|
+
|
|
83
|
+
Update with `chi update` (no bundle changes).
|
|
84
|
+
|
|
11
85
|
## [0.14.0] - 2026-10-02
|
|
12
86
|
|
|
13
87
|
### Added
|
|
@@ -782,7 +856,9 @@ and long-lived sessions.
|
|
|
782
856
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
783
857
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
784
858
|
|
|
785
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
859
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.16.0...HEAD
|
|
860
|
+
[0.16.0]: https://github.com/dm1try/samagotchi/compare/v0.15.0...v0.16.0
|
|
861
|
+
[0.15.0]: https://github.com/dm1try/samagotchi/compare/v0.14.0...v0.15.0
|
|
786
862
|
[0.14.0]: https://github.com/dm1try/samagotchi/compare/v0.13.0...v0.14.0
|
|
787
863
|
[0.13.0]: https://github.com/dm1try/samagotchi/compare/v0.12.0...v0.13.0
|
|
788
864
|
[0.12.0]: https://github.com/dm1try/samagotchi/compare/v0.11.0...v0.12.0
|
data/docs/architecture.md
CHANGED
|
@@ -228,6 +228,16 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
228
228
|
`guardrails.parent_approvals` is `403 parent_approval_refused`, the question still open.
|
|
229
229
|
- `POST /session/:id/question/dismiss` — leave the question unanswered (an approval: denied);
|
|
230
230
|
`200`, or `409` when it is no longer pending.
|
|
231
|
+
- `POST /session/:id/relay` — the approval relay, on a delegated child's worker
|
|
232
|
+
(`{action, relay_id, question_id}`): `opened` / `closed` (with a `reason`) set or clear the
|
|
233
|
+
pending question's `relayed_to` mark (`QuestionDesk#annotate`, `:question_relay`); `answered`
|
|
234
|
+
makes the child ask its parent's Bridge for the answer (`RelayVerifier`) and take it only for
|
|
235
|
+
this child and the question pending now. `200`, `409` no longer pending, `403` a parent agent's
|
|
236
|
+
allow beyond the child's `guardrails.parent_approvals`, `422 relay_unverified` (the question
|
|
237
|
+
stays open).
|
|
238
|
+
- `POST /session/:id/relay/status` — on a parent's worker, `{relay_id}`: what its `RelayDesk`
|
|
239
|
+
(memory only) holds for that relay (`child_id`, `child_question_id`, `state`, `answer`, `by`);
|
|
240
|
+
`404` for an unknown id. A POST with a body because routes match exact paths.
|
|
231
241
|
- `POST /session/:id/command` — a session command (`/model`, `/models`, `!rollback`, `!cmd`,
|
|
232
242
|
`/continue`) for the worker loop; `202` with a `command_id` its `:command_ran` names, `400` when
|
|
233
243
|
the line isn't one.
|
data/docs/cli.md
CHANGED
|
@@ -211,8 +211,9 @@ Notes:
|
|
|
211
211
|
ended, 3 a question or an approval waits for an answer (stderr gets the
|
|
212
212
|
whole question as `chi send --wait` prints it, with the
|
|
213
213
|
`chi answer ID --question QID --option N` command; the worker keeps it
|
|
214
|
-
open, and `chi answer` or the web answers it), 1 the turn failed
|
|
215
|
-
|
|
214
|
+
open, and `chi answer` or the web answers it), 1 the turn failed, ended
|
|
215
|
+
with no answer (stderr: `chi: the model gave an empty answer`, as with
|
|
216
|
+
`--non-interactive`) or the worker went away. For a
|
|
216
217
|
script, `chi send --new --wait` is the better entry point: stdout holds the
|
|
217
218
|
answer alone (see [Starting a session](sessions.md#starting-a-session)).
|
|
218
219
|
- `-p` always feeds **and** runs the prompt; there is no feed-and-edit variant. To
|
data/docs/configuration.md
CHANGED
|
@@ -625,7 +625,9 @@ asks again in the same turn with a hidden note ("your last reply had no visible
|
|
|
625
625
|
unless `sampling:` sets one; the REPL prints `↻ empty answer, asking again (1/1)` and the web shows it as a row of
|
|
626
626
|
the step. `retry.empty_answer` (default `1`, at most `3`, env `SAMAGOTCHI_RETRY_EMPTY_ANSWER`, no CLI flag) is how
|
|
627
627
|
many times per turn; `0` ends the turn at the empty answer as before. An answer cut because the context is full
|
|
628
|
-
(90 % or more) is not retried. When the retries run out the turn ends with
|
|
628
|
+
(90 % or more) is not retried. When the retries run out the turn ends with no answer: every UI shows one muted line,
|
|
629
|
+
`no answer: the model returned nothing (after 1 retry)`, and the model is told on its next turn with a hidden note;
|
|
630
|
+
no made-up answer is saved.
|
|
629
631
|
A generation a plugin cuts while it streams (loop-guard's thinking watch, or any `stop_generation`,
|
|
630
632
|
[hooks.md](hooks.md#watching-the-stream)) uses the same budget: `↻ cut by loop-guard, asking again (1/1)`, and with
|
|
631
633
|
none left the turn ends as cancelled (hook).
|
data/docs/guardrails.md
CHANGED
|
@@ -56,9 +56,14 @@ Who answers:
|
|
|
56
56
|
the approval waits (in the session file) and shows on attach.
|
|
57
57
|
- `-p … --non-interactive`: nobody; the call is denied ("No one to approve it
|
|
58
58
|
(non-interactive run)").
|
|
59
|
+
- A delegated child session (the `delegate` tool), while its parent waits:
|
|
60
|
+
the parent's user, on the parent's own card (the approval relay; see
|
|
61
|
+
[Sessions: Delegating](sessions.md#delegating)). The child's card stays
|
|
62
|
+
answerable too, the first answer wins. The parent's model sees only the
|
|
63
|
+
outcome.
|
|
59
64
|
- A parent agent running chi as a sub-agent (`chi send --wait` exits 3 with
|
|
60
|
-
the approval): `chi answer` may deny it, never allow it by default
|
|
61
|
-
[Approvals from a parent agent](#approvals-from-a-parent-agent).
|
|
65
|
+
the approval): `chi answer` may deny it, never allow it by default, or leave
|
|
66
|
+
it for the user. See [Approvals from a parent agent](#approvals-from-a-parent-agent).
|
|
62
67
|
|
|
63
68
|
The model gets one line on a deny. A rule's or hook's deny reads
|
|
64
69
|
`[execute] Error: denied by guardrail (rule git-push, bundle guardrails): git push publishes commits. The user was not asked. Do not retry it or reach the same result another way; ask the user how to proceed.`
|
|
@@ -84,6 +89,15 @@ The file tools can't write the store.
|
|
|
84
89
|
|
|
85
90
|
### Approvals from a parent agent
|
|
86
91
|
|
|
92
|
+
A chi parent's `delegate` children don't need any of this: their approvals go
|
|
93
|
+
to the user on the parent's own card (the approval relay). This section is the
|
|
94
|
+
path for other agents, and for a chi parent that can't host the relay (a
|
|
95
|
+
`--no-shared` REPL, `--non-interactive`): there the model gets the approval
|
|
96
|
+
block and the rules below. With the relay, the same rules still hold for a
|
|
97
|
+
parent model that answers its own relay card (a piped or marked `chi --attach`
|
|
98
|
+
on the parent): that answer is a parent agent's, checked by the parent and
|
|
99
|
+
again by the child.
|
|
100
|
+
|
|
87
101
|
When another agent (Claude Code, Codex, a script) runs chi with
|
|
88
102
|
`chi send --wait`, an approval comes back to it as exit 3 with `kind:
|
|
89
103
|
approval`, and `chi answer` answers it. A rule with `verdict: ask` means "ask
|
data/docs/plugins.md
CHANGED
|
@@ -479,7 +479,7 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
479
479
|
| `ctx.cwd` | the session's working directory |
|
|
480
480
|
| `ctx.repo_root` | the git checkout holding `cwd`, or nil |
|
|
481
481
|
| `ctx.settings` | the bundle's settings, frozen |
|
|
482
|
-
| `ctx.data_dir` | `$XDG_STATE_HOME/samagotchi/plugins/<bundle>/`, created on first use |
|
|
482
|
+
| `ctx.data_dir` | `$XDG_STATE_HOME/samagotchi/plugins/<bundle>/`, created on first use. A session's own state goes in `sessions/<id>.json` or `sessions/<id>/` there: chi removes it when the session is deleted, discarded or pruned |
|
|
483
483
|
| `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
|
|
484
484
|
| `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 |
|
|
485
485
|
| `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 |
|
|
@@ -937,8 +937,8 @@ with a line and carry on unchanged.
|
|
|
937
937
|
|
|
938
938
|
`/checkin` (anytime) for this session. What it changes (on/off, the mode,
|
|
939
939
|
the threshold) is saved with the session, in the bundle's data dir
|
|
940
|
-
(`plugins/check-in/sessions/<id>.json`), so it outlasts a worker's restart
|
|
941
|
-
another session starts from config.yml:
|
|
940
|
+
(`plugins/check-in/sessions/<id>.json`), so it outlasts a worker's restart
|
|
941
|
+
and goes with the session; another session starts from config.yml:
|
|
942
942
|
|
|
943
943
|
| | |
|
|
944
944
|
|---|---|
|
data/docs/sessions.md
CHANGED
|
@@ -40,7 +40,7 @@ chi sessions prune [--dry-run] [--days N] [--keep N] [--keep-status running,...]
|
|
|
40
40
|
chi sessions clean [--dry-run] [--days N] # test sessions: all, or older than N days
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
A session whose worker waits for an answer (a question, a hook's question or a guardrail approval) reads `waiting` in the status column, and json has `waiting: "question"|"approval"|"hook"` and `waiting_id`, the question's id for `chi answer --question` (both null otherwise). Only while a worker runs it: a question a dead worker left in the file shows the saved status. Answer it with `chi answer`, the web or `chi --attach ID`; `chi sessions stop ID` frees the worker instead.
|
|
43
|
+
A session whose worker waits for an answer (a question, a hook's question or a guardrail approval) reads `waiting` in the status column, and json has `waiting: "question"|"approval"|"hook"` and `waiting_id`, the question's id for `chi answer --question` (both null otherwise). A delegate's approval its parent relayed to its own card (see *Delegating*) reads `waiting (in parent ab12)`, json `relayed_to: "ab12…"` (the parent's short id, else null). Only while a worker runs it: a question a dead worker left in the file shows the saved status. Answer it with `chi answer`, the web or `chi --attach ID`; `chi sessions stop ID` frees the worker instead.
|
|
44
44
|
|
|
45
45
|
Examples:
|
|
46
46
|
|
|
@@ -180,7 +180,12 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
|
|
|
180
180
|
|
|
181
181
|
- `delegate(task, model:, session:, wait:, timeout:)`: starts a child and, by default, waits for its reply (`wait: false` returns at once, so several children can run side by side; `timeout`, default 600 s, leaves a slow child running; 0 checks once without waiting). With `session:` (a child's id or prefix) the task goes to that child as a follow-up instead, delivered like `chi send` (client id `delegate:<parent short id>`, shown as `delegate>` in the child's UIs); only this session's children take one.
|
|
182
182
|
- `delegate_result(session:, timeout:)`: waits for a child's next reply: the one named, or the newest running child (`timeout` as for `delegate`: 0 checks once and returns at once). The reply comes back once; a later call waits for the next one.
|
|
183
|
-
- What a wait returns, `session: <id>` then `status:` and the text, with the status words `chi send --wait --format json` uses: `answered` with the reply after a `---` line; `question` as soon as the child opens a question or a
|
|
183
|
+
- What a wait returns, `session: <id>` then `status:` and the text, with the status words `chi send --wait --format json` uses: `answered` with the reply after a `---` line; `question` as soon as the child opens a question (the model's own, or a hook's), with the question's text, options and the `chi answer` command for it (nothing is answered for it: `chi answer`, attach or use the web, then wait again); `failed`, `canceled` or `no_answer` when the child's turn ended with nothing to say (its session shows what happened); `error` (its worker crashed) or `stopped` (`chi sessions stop`) at once; `worker_gone` when the child has had no worker for 15 seconds (it crashed without saying so, or idle-exited): `delegate` with `session:` starts it again; `running` on timeout, or when the parent's own turn is canceled (the child keeps running; the next turn can wait again). A reply saved before the child's worker idle-exited is returned at once.
|
|
184
|
+
- **A child's approvals go to the parent's user** (the approval relay). While the parent waits (`delegate`, `delegate_result`), a guardrail approval the child opens reopens as the parent session's own approval: the same card in the parent's web page and attached terminal, with the same facts and diff, a line naming the delegate and its task (`delegate ab12 · fix the flaky spec…`, linked to its session) and the header `Approve delegate ab12's tool call?`. The options are the child's, by index; "for the session" says it is the delegate's session (the session scope keys on the child; repo and rule approvals are one shared store). The user answers once there; the parent's model never sees the approval, only one line per relayed approval in the tool's result, before the reply: `approval relayed to your user: execute: git push origin main → allowed once` (or `denied ("why")`, `dismissed (denied)`, `answered on the child`, `not delivered (the child's worker is gone)`, `refused by the child …`, `still open (your turn was stopped)`). The time the card is open doesn't count against `timeout`. While the parent waits on one child, its other running children's approvals are relayed too, oldest first, one card at a time (`+1 more delegate waiting`); each outcome comes in that child's next result.
|
|
185
|
+
- The child's own card stays answerable (the first answer wins: answered there, the parent's card closes as "answered in ab12"). While relayed it says `Waiting for approval in parent ab12: answering here works too`; `chi sessions list` shows the child as `waiting (in parent ab12)` (`--format json`: `relayed_to`), and the web's bell rings for the parent's card, not the child's.
|
|
186
|
+
- How the child trusts it: the parent's POST that says "answered" (Bridge `POST /session/<child>/relay`) carries no answer. The child asks its parent session's live Bridge (`POST /session/<parent>/relay/status`), which reports only what the parent's question flow took from a UI (`RelayDesk`, in memory only), and takes it only for this child, the question pending now, answered. A parent agent's answer on the relay card (a piped or marked `chi --attach PARENT`, `chi answer`) is held to `guardrails.parent_approvals` by the parent and again by the child.
|
|
187
|
+
- A Stop of the parent's turn closes its card; the child's approval stays open there. A child's worker that goes closes the card (`… worker is gone`); a parent's worker that goes clears the child's "waiting in parent" line within 2 s. A later `delegate_result` relays it again.
|
|
188
|
+
- No relay where the parent can't host one (a `--no-shared` REPL, `--non-interactive`) or isn't waiting (`wait: false` until the next `delegate_result`): the approval waits on the child, with the web's bell, and the result's question block says to deny it or leave it for the user. The model's own questions and hooks' questions come back to the model as before. A grandchild's (a plugin's `ctx.sessions.fork` can make one) is relayed hop by hop: `delegate ab12 → cd34 asks:`.
|
|
184
189
|
- Every child starts with the shipped `delegated` system memory preloaded (`preloaded_memory_names: ["system/delegated"]`; `--mute delegated` would hide it): put everything in the final reply, don't delegate further, don't write memories or change config, stay in the folder. Its system prompt also says `Delegated by session <parent id>`. The parent's own `--mute` list does not carry over.
|
|
185
190
|
- Limits: a child cannot `delegate` (depth 1; the tool refuses), and one session may have at most `session.max_children` (default `4`, env `SAMAGOTCHI_SESSION_MAX_CHILDREN`) children running at a time, counted from the session files (a finished or idle-exited child does not count; one whose worker is still starting does, for 15 s). A refused call creates no session. Nothing stops children with the parent: `chi sessions stop ID` does.
|
|
186
191
|
- The link is the session's `parent_id`, written before the worker starts, so a respawn keeps it; `GET /api/sessions` and `/api/sessions/:id` carry it, and `SessionManager.children_of` lists a parent's children.
|
data/docs/sub-agent.md
CHANGED
|
@@ -53,12 +53,22 @@ command, folder, rule and reason.
|
|
|
53
53
|
|
|
54
54
|
## Approvals
|
|
55
55
|
|
|
56
|
+
A chi parent (the `delegate` tool) doesn't go through any of this: its
|
|
57
|
+
children's approvals go to its own user as its own approvals, and its model
|
|
58
|
+
sees only how each was answered. See
|
|
59
|
+
[Sessions: Delegating](sessions.md#delegating). What follows is for other
|
|
60
|
+
parents (Claude Code, Codex, a script), and for a chi parent that can't host
|
|
61
|
+
the relay (a `--no-shared` REPL, `--non-interactive`).
|
|
62
|
+
|
|
56
63
|
An approval is a guardrail rule with `verdict: ask`: its user said "ask me". So
|
|
57
64
|
a parent may deny it (`--option Deny --text "why"`, `--text` alone, or
|
|
58
65
|
`--dismiss`) but not allow it: `chi answer` refuses an Allow with exit 1 and
|
|
59
66
|
`allowing a tool call is up to the user: deny it (--option Deny --text WHY),
|
|
60
67
|
and tell your user`. For an approval, the question block and `answer_with` give
|
|
61
|
-
the deny command, not `--option N` or `chi --attach`.
|
|
68
|
+
the deny command, not `--option N` or `chi --attach`. The block also offers to
|
|
69
|
+
leave it open: tell your user it waits in chi web (the bell and badge show
|
|
70
|
+
it there), and `chi send --wait --format json ID` (no message) waits until
|
|
71
|
+
they answer it. With
|
|
62
72
|
`guardrails.parent_approvals: once` in chi's config.yml (it has no environment
|
|
63
73
|
variable), "Allow once" goes through; the wider scopes never do,
|
|
64
74
|
and neither does any allow on chi's own config, hooks or guardrail rules. See
|
|
@@ -113,8 +123,10 @@ Start a task: `chi send --new --wait --format json --timeout 500 -m "<task>"`
|
|
|
113
123
|
- `question.kind: approval`: chi wants to run something its user's guardrails
|
|
114
124
|
flagged, and allowing it is up to your user. Deny it (the `answer_with`
|
|
115
125
|
command: `--option Deny --text "why"`), and tell your user what it wanted to
|
|
116
|
-
run and why.
|
|
117
|
-
`chi --
|
|
126
|
+
run and why. Or leave it open: tell your user it waits in chi web (the
|
|
127
|
+
session's short id), then `chi send --wait --format json --timeout 500 <session_id>`
|
|
128
|
+
waits until they answer it there. Never allow it yourself, through
|
|
129
|
+
`chi answer`, a piped `chi --attach` or chi's web API.
|
|
118
130
|
- `status: running` (exit 4): still working. Wait again:
|
|
119
131
|
`chi send --wait --format json --timeout 500 <session_id>`.
|
|
120
132
|
- Anything else (exit 1): report `detail` to your user. `chi --attach <session_id>`
|
|
@@ -23,10 +23,14 @@ module Samagotchi
|
|
|
23
23
|
|
|
24
24
|
# A message as a model-facing reader may see it: without `display`.
|
|
25
25
|
# The same object when it has none.
|
|
26
|
+
# An empty-answer note's marker (TurnNote::EMPTY_ANSWER) is display
|
|
27
|
+
# only too: it goes the same way.
|
|
28
|
+
DISPLAY_ONLY = [KEY.to_s, "empty_answer"].freeze
|
|
29
|
+
|
|
26
30
|
def self.strip(message)
|
|
27
|
-
return message unless message.is_a?(Hash) &&
|
|
31
|
+
return message unless message.is_a?(Hash) && DISPLAY_ONLY.any? { |key| message.key?(key.to_sym) || message.key?(key) }
|
|
28
32
|
|
|
29
|
-
message.reject { |key, _| key.to_s
|
|
33
|
+
message.reject { |key, _| DISPLAY_ONLY.include?(key.to_s) }
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
def self.strip_all(messages)
|
|
@@ -51,7 +51,7 @@ module Samagotchi
|
|
|
51
51
|
# The events after which the file is written again: the ones that
|
|
52
52
|
# change an entry or the turn count.
|
|
53
53
|
SAVED_ON = (%i[card hook_notice empty_answer_retry question_requested question_answered question_cancelled
|
|
54
|
-
turn_failed] + Events::TURN_KEPT).freeze
|
|
54
|
+
question_relay turn_failed] + Events::TURN_KEPT).freeze
|
|
55
55
|
|
|
56
56
|
# What a session's saved file lists, as a store started from it lists
|
|
57
57
|
# (no turn running): the web's cards for a session no worker runs.
|
|
@@ -171,6 +171,7 @@ module Samagotchi
|
|
|
171
171
|
add_turn_row({ type: :question, pending_question: Marshal.load(Marshal.dump(event[:pending_question])) }) if @running
|
|
172
172
|
when :question_answered then resolve_question(event[:id], answer: event[:answer])
|
|
173
173
|
when :question_cancelled then resolve_question(event[:id], cancelled: true, reason: event[:reason])
|
|
174
|
+
when :question_relay then mark_relay(event[:id], event[:relayed_to])
|
|
174
175
|
end
|
|
175
176
|
end
|
|
176
177
|
|
|
@@ -219,6 +220,20 @@ module Samagotchi
|
|
|
219
220
|
entry&.merge!(Marshal.load(Marshal.dump(outcome.compact)))
|
|
220
221
|
end
|
|
221
222
|
|
|
223
|
+
# The open question was relayed to a parent's user, or no longer is.
|
|
224
|
+
def mark_relay(id, relayed_to)
|
|
225
|
+
entry = @entries.reverse_each.find do |e|
|
|
226
|
+
e[:type] == :question && e.dig(:pending_question, :id).to_s == id.to_s
|
|
227
|
+
end
|
|
228
|
+
return unless entry
|
|
229
|
+
|
|
230
|
+
if relayed_to
|
|
231
|
+
entry[:pending_question][:relayed_to] = Marshal.load(Marshal.dump(relayed_to))
|
|
232
|
+
else
|
|
233
|
+
entry[:pending_question].delete(:relayed_to)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
222
237
|
def push(entry)
|
|
223
238
|
@entries << entry
|
|
224
239
|
notice = notice?(entry)
|
|
@@ -268,6 +268,12 @@ module Samagotchi
|
|
|
268
268
|
@turn[:pending_question] = event[:pending_question]&.dup
|
|
269
269
|
when :question_answered, :question_cancelled
|
|
270
270
|
@turn[:pending_question] = nil
|
|
271
|
+
when :question_relay
|
|
272
|
+
pending = @turn[:pending_question]
|
|
273
|
+
if pending && pending[:id].to_s == event[:id].to_s
|
|
274
|
+
relayed_to = event[:relayed_to]
|
|
275
|
+
@turn[:pending_question] = relayed_to ? pending.merge(relayed_to: relayed_to) : pending.except(:relayed_to)
|
|
276
|
+
end
|
|
271
277
|
end
|
|
272
278
|
parts << { kind: "notice", event: TurnNotice.slice(event) } if TurnNotice.notice?(event)
|
|
273
279
|
end
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -24,6 +24,8 @@ require_relative "image_store"
|
|
|
24
24
|
require_relative "log"
|
|
25
25
|
require_relative "version"
|
|
26
26
|
require_relative "worker_sidecar"
|
|
27
|
+
require_relative "relay_verifier"
|
|
28
|
+
require_relative "relay_watcher"
|
|
27
29
|
|
|
28
30
|
module Samagotchi
|
|
29
31
|
# Bridge is an optional HTTP transport that lets an external web / desktop
|
|
@@ -192,6 +194,7 @@ module Samagotchi
|
|
|
192
194
|
await_answers(REQUEST_GRACE_SECONDS)
|
|
193
195
|
@connection_threads.each { |t| t.kill rescue nil }
|
|
194
196
|
@connection_threads.clear
|
|
197
|
+
@mutex.synchronize { @relay_watchers&.each { |t| t.kill rescue nil } }
|
|
195
198
|
@accept_thread&.join(2)
|
|
196
199
|
remove_sidecar
|
|
197
200
|
nil
|
|
@@ -429,6 +432,8 @@ module Samagotchi
|
|
|
429
432
|
%w[POST command] => :handle_command,
|
|
430
433
|
%w[POST exit] => :handle_exit_request,
|
|
431
434
|
%w[POST recap] => :handle_recap,
|
|
435
|
+
%w[POST relay] => :handle_relay,
|
|
436
|
+
%w[POST relay/status] => :handle_relay_status,
|
|
432
437
|
%w[GET state] => :handle_state,
|
|
433
438
|
%w[GET stats] => :handle_stats,
|
|
434
439
|
%w[GET snapshot] => :handle_snapshot
|
|
@@ -525,6 +530,38 @@ module Samagotchi
|
|
|
525
530
|
end
|
|
526
531
|
end
|
|
527
532
|
|
|
533
|
+
# What a parent's relay may say about this session's approval
|
|
534
|
+
# (BridgeClient#relay): opened and closed only change how the question
|
|
535
|
+
# shows (QuestionDesk#annotate); answered makes this worker ask the
|
|
536
|
+
# parent for the answer (RelayVerifier). No auth: a poke at opened or
|
|
537
|
+
# closed can at most show a wrong banner.
|
|
538
|
+
RELAY_ACTIONS = %w[opened closed answered].freeze
|
|
539
|
+
# Why a relay closed, as a parent says it; anything else is "closed".
|
|
540
|
+
RELAY_CLOSE_REASONS = %w[stopped closed child_gone answered_on_child].freeze
|
|
541
|
+
|
|
542
|
+
# POST /session/:id/relay {action, relay_id, question_id}. 200, 409 (no
|
|
543
|
+
# longer pending, or another relay's), 403 refused (a parent agent's
|
|
544
|
+
# allow), 422 relay_unverified, 400 a bad request.
|
|
545
|
+
def handle_relay(session_id, body)
|
|
546
|
+
parsed = parse_json(body)
|
|
547
|
+
return [{ "Allow" => "POST" }, 400, { error: "invalid_json" }] unless parsed.is_a?(Hash)
|
|
548
|
+
|
|
549
|
+
action, relay_id, qid = %w[action relay_id question_id].map { |key| fetched(parsed, key).to_s }
|
|
550
|
+
unless RELAY_ACTIONS.include?(action) && !relay_id.empty? && !qid.empty?
|
|
551
|
+
return [{ "Allow" => "POST" }, 400, { error: "missing_fields", detail: "action (opened, closed, answered), relay_id and question_id required" }]
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
case action
|
|
555
|
+
when "opened" then relay_opened(qid, relay_id)
|
|
556
|
+
when "closed"
|
|
557
|
+
reason = fetched(parsed, "reason").to_s
|
|
558
|
+
relay_closed(qid, relay_id, RELAY_CLOSE_REASONS.include?(reason) ? reason : "closed")
|
|
559
|
+
else
|
|
560
|
+
RelayVerifier.new(engine: @engine, state_dir: @state_dir, session_id: session_id)
|
|
561
|
+
.answer(relay_id: relay_id, question_id: qid)
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
528
565
|
# Dismiss the pending question (an empty answer, as in the REPL): the
|
|
529
566
|
# tool returns without an answer and every UI gets :question_cancelled.
|
|
530
567
|
# Only the question the client saw, and only while nobody has answered
|
|
@@ -872,6 +909,53 @@ module Samagotchi
|
|
|
872
909
|
headers["connection"].to_s.downcase == "close"
|
|
873
910
|
end
|
|
874
911
|
|
|
912
|
+
# POST /session/:id/relay/status {relay_id}: what this (the parent's)
|
|
913
|
+
# worker's relay holds, for the child that verifies an answer
|
|
914
|
+
# (RelayVerifier). Read-only and no secret; 404 for an unknown id.
|
|
915
|
+
def handle_relay_status(_session_id, body)
|
|
916
|
+
parsed = parse_json(body)
|
|
917
|
+
return [{ "Allow" => "POST" }, 400, { error: "invalid_json" }] unless parsed.is_a?(Hash)
|
|
918
|
+
|
|
919
|
+
relay = @engine.relay_desk.status(fetched(parsed, "relay_id").to_s)
|
|
920
|
+
return [{}, 404, { error: "unknown_relay" }] unless relay
|
|
921
|
+
|
|
922
|
+
[{}, 200, relay]
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
# A parent's relay card for +qid+ opened: the question says so (the
|
|
926
|
+
# child's card and lists show where else it can be answered).
|
|
927
|
+
def relay_opened(qid, relay_id)
|
|
928
|
+
parent_id = begin
|
|
929
|
+
Session.load(@session_id, state_dir: @state_dir).parent_id
|
|
930
|
+
rescue ArgumentError
|
|
931
|
+
nil
|
|
932
|
+
end
|
|
933
|
+
return [{}, 422, { error: "relay_unverified", detail: "this session has no parent" }] unless parent_id
|
|
934
|
+
|
|
935
|
+
marker = { parent_id: parent_id, parent_short: parent_id[0, 8], relay_id: relay_id }
|
|
936
|
+
return [{}, 409, { error: "question_not_pending", detail: "no pending question #{qid}" }] unless @engine.annotate_question(qid, relayed_to: marker)
|
|
937
|
+
|
|
938
|
+
Log.info(:bridge, "relay_opened", sid: @session_id, id: qid, parent: parent_id[0, 8])
|
|
939
|
+
watch = RelayWatcher.start(engine: @engine, question_id: qid, relay_id: relay_id,
|
|
940
|
+
parent_dir: Session.session_dir(parent_id, state_dir: @state_dir))
|
|
941
|
+
@mutex.synchronize { @relay_watchers = (@relay_watchers || []).select(&:alive?) << watch }
|
|
942
|
+
[{}, 200, { status: "opened", question_id: qid }]
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
# The parent's relay card closed unanswered: the mark goes, if it is
|
|
946
|
+
# still that relay's.
|
|
947
|
+
def relay_closed(qid, relay_id, reason)
|
|
948
|
+
pending = @engine.pending_question
|
|
949
|
+
relay = pending && pending[:relayed_to]
|
|
950
|
+
unless pending && pending[:id].to_s == qid && relay && (relay[:relay_id] || relay["relay_id"]).to_s == relay_id
|
|
951
|
+
return [{}, 409, { error: "question_not_pending", detail: "no question #{qid} relayed by #{relay_id}" }]
|
|
952
|
+
end
|
|
953
|
+
return [{}, 409, { error: "question_not_pending", detail: "no pending question #{qid}" }] unless @engine.annotate_question(qid, relayed_to: nil, reason: reason)
|
|
954
|
+
|
|
955
|
+
Log.info(:bridge, "relay_closed", sid: @session_id, id: qid, reason: reason)
|
|
956
|
+
[{}, 200, { status: "closed", question_id: qid }]
|
|
957
|
+
end
|
|
958
|
+
|
|
875
959
|
def write_sidecar
|
|
876
960
|
# version: the chi this worker runs (chi update reports older ones).
|
|
877
961
|
WorkerSidecar.new(port: @port, bind: @bind, session_id: @session_id, started_at: Time.now.iso8601(3),
|
|
@@ -180,6 +180,33 @@ module Samagotchi
|
|
|
180
180
|
post("cancel", { reason: reason }, read_body: true)
|
|
181
181
|
end
|
|
182
182
|
|
|
183
|
+
# How long the approval relay's calls wait for a reply: a parent's Stop
|
|
184
|
+
# isn't held for READ_TIMEOUT by a child that doesn't answer.
|
|
185
|
+
RELAY_TIMEOUT = 5
|
|
186
|
+
# How long a child waits for its parent's word on a relay (#relay_status).
|
|
187
|
+
RELAY_STATUS_TIMEOUT = 2
|
|
188
|
+
|
|
189
|
+
# POST /session/:id/relay on a child's worker: a parent's relay of the
|
|
190
|
+
# child's approval opened, closed or was answered (the child asks the
|
|
191
|
+
# parent which answer, #relay_status). 200 answered, 409 no longer
|
|
192
|
+
# pending, 403 refused (a parent agent's allow), 422 relay_unverified.
|
|
193
|
+
# @param action [String] "opened", "closed" or "answered"
|
|
194
|
+
# @return [Response]
|
|
195
|
+
# @raise [SystemCallError] the worker is gone or didn't answer in time
|
|
196
|
+
def relay(action:, relay_id:, question_id:, read_timeout: RELAY_TIMEOUT, **extra)
|
|
197
|
+
post("relay", { action: action, relay_id: relay_id, question_id: question_id, **extra },
|
|
198
|
+
read_body: true, read_timeout: read_timeout)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# POST /session/:id/relay/status on a parent's worker: what its relay
|
|
202
|
+
# +relay_id+ holds (child_id, child_question_id, state, answer, by); 404
|
|
203
|
+
# for one it doesn't know. A POST with a body: routes match exact paths.
|
|
204
|
+
# @return [Response]
|
|
205
|
+
# @raise [SystemCallError]
|
|
206
|
+
def relay_status(relay_id, read_timeout: RELAY_STATUS_TIMEOUT)
|
|
207
|
+
post("relay/status", { relay_id: relay_id }, read_body: true, read_timeout: read_timeout)
|
|
208
|
+
end
|
|
209
|
+
|
|
183
210
|
# GET /session/:id/<path> as JSON.
|
|
184
211
|
# @return [Hash, nil] the parsed body, or nil unless the Bridge answered 200
|
|
185
212
|
def get_json(path)
|
|
@@ -283,11 +310,11 @@ module Samagotchi
|
|
|
283
310
|
# the same one.
|
|
284
311
|
def deadline = (Time.now.to_f + (@read_timeout * DEADLINE_SHARE)).round(3)
|
|
285
312
|
|
|
286
|
-
def post(path, payload, read_body:)
|
|
313
|
+
def post(path, payload, read_body:, read_timeout: @read_timeout)
|
|
287
314
|
sock = TCPSocket.new(@host, @port)
|
|
288
315
|
json_body = JSON.generate(payload)
|
|
289
316
|
sock.write("POST /session/#{@session_id}/#{path} HTTP/1.1\r\nHost: #{@host}:#{@port}\r\nContent-Type: application/json\r\nContent-Length: #{json_body.bytesize}\r\nConnection: close\r\n\r\n#{json_body}")
|
|
290
|
-
reply = read_reply(sock, path, whole: read_body)
|
|
317
|
+
reply = read_reply(sock, path, whole: read_body, timeout: read_timeout)
|
|
291
318
|
body = read_body ? reply.split("\r\n\r\n", 2)[1] : nil
|
|
292
319
|
Response.new(status: reply[STATUS_LINE, 1].to_i, body: body)
|
|
293
320
|
ensure
|
|
@@ -295,17 +322,17 @@ module Samagotchi
|
|
|
295
322
|
end
|
|
296
323
|
|
|
297
324
|
# The reply up to the Bridge's close (or its first line only), within
|
|
298
|
-
# @read_timeout
|
|
325
|
+
# +timeout+ seconds in all (@read_timeout by default).
|
|
299
326
|
# @raise [Errno::ETIMEDOUT]
|
|
300
|
-
def read_reply(sock, path, whole: true)
|
|
301
|
-
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) +
|
|
327
|
+
def read_reply(sock, path, whole: true, timeout: @read_timeout)
|
|
328
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
302
329
|
reply = +""
|
|
303
330
|
loop do
|
|
304
331
|
break if !whole && reply.include?("\n")
|
|
305
332
|
|
|
306
333
|
left = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
307
334
|
unless left.positive? && sock.wait_readable(left)
|
|
308
|
-
raise Errno::ETIMEDOUT, "bridge #{path}: no reply within #{
|
|
335
|
+
raise Errno::ETIMEDOUT, "bridge #{path}: no reply within #{timeout}s"
|
|
309
336
|
end
|
|
310
337
|
|
|
311
338
|
reply << sock.readpartial(16_384)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.16.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
|
-
self_map.md: sha256:
|
|
8
|
+
self_map.md: sha256:0410c2e62716e256a2a6aec73c7801def185cbae9c1db556efcedbbad22d908c
|
|
9
9
|
config_modification_protocol.md: sha256:d0e9d4a84652bf54ec2fd664f61c536637af9decdd22fcd982f52195bf9f5fb4
|
|
10
10
|
memory_guide.md: sha256:81b911b9a015f0924c48162b3dc4b6376309e743765074419a99fab46e480858
|
|
11
11
|
delegated.md: sha256:0a71d5dfa7127df2d51cb5f0e7214240ed88777e36559430a8724c796d7ae561
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
- `delegate` hands a task to a child session that runs in parallel and returns only its final
|
|
26
26
|
reply (`delegate_result` waits for it; `session:` sends a follow-up to a child). A child is a
|
|
27
27
|
normal session: it shows in `chi sessions list` with `↳ <parent>`, and the user can steer it
|
|
28
|
-
with `chi --attach <id
|
|
28
|
+
with `chi --attach <id>`. While I wait for a child, its tool approvals go to my user as my own
|
|
29
|
+
approvals (I see only how each was answered); see `docs/sessions.md` "Delegating".
|
|
29
30
|
- Bundles extend me: memories, `hooks/*.rb`, `guardrails/*.yml`, and a `plugin.rb` that adds slash
|
|
30
31
|
commands (`anytime:` ones run mid-turn), model tools (they can return images I see), hooks,
|
|
31
32
|
cards, side answers (`ask_model`),
|
|
@@ -41,7 +41,8 @@ module Samagotchi
|
|
|
41
41
|
used_tokens.to_f / window_tokens >= CONTEXT_FULL_RATIO
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
# @return [Integer] the budget, and the attempts spent this turn
|
|
45
|
+
attr_reader :limit, :attempts
|
|
45
46
|
|
|
46
47
|
def initialize(limit: self.class.limit)
|
|
47
48
|
@limit = limit
|