samagotchi 0.97.0 → 0.98.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 +30 -1
- data/docs/architecture.md +10 -2
- data/docs/bundle-authoring.md +5 -3
- data/docs/cli.md +8 -3
- data/docs/hooks.md +59 -0
- data/docs/internals/bundles.md +1 -1
- data/docs/internals/engine.md +18 -0
- data/docs/plugins.md +8 -1
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/answer_refs.rb +16 -9
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +39 -5
- data/lib/samagotchi/bundles/source-links/manifest.yml +5 -5
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +26 -2
- data/lib/samagotchi/hooks/bundle_loader.rb +18 -15
- data/lib/samagotchi/hooks.rb +4 -1
- data/lib/samagotchi/llm/errors.rb +51 -17
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +28 -9
- data/lib/samagotchi/memory_bundle/manifest.rb +17 -0
- data/lib/samagotchi/output_formatter.rb +8 -2
- data/lib/samagotchi/pending_question.rb +7 -3
- data/lib/samagotchi/plugin/api.rb +5 -0
- data/lib/samagotchi/question_asked.rb +83 -0
- data/lib/samagotchi/question_desk.rb +3 -2
- data/lib/samagotchi/testing/bundle.rb +18 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +30 -9
- data/lib/samagotchi/web/public/app.js +44 -11
- data/lib/samagotchi/web/public/index.html +36 -13
- data/lib/samagotchi/web/public/info_bar_model.js +0 -2
- data/lib/samagotchi/web/public/lists_view.js +32 -8
- data/lib/samagotchi/web/public/markdown_place.js +25 -8
- data/lib/samagotchi/web/public/question_cards.js +23 -6
- data/lib/samagotchi/web/public/session_rename.js +88 -15
- data/lib/samagotchi/web/public/session_title.js +6 -0
- data/lib/samagotchi/web/public/stage_frame.js +24 -14
- data/lib/samagotchi/web/public/stage_model.js +4 -2
- data/lib/samagotchi/web/public/stage_slots.js +41 -9
- data/lib/samagotchi/web/public/stage_view.js +149 -8
- data/lib/samagotchi/web/public/stream_handlers.js +5 -1
- data/lib/samagotchi/web/public/turn_model.js +20 -0
- data/lib/samagotchi/web/public/turn_view.js +22 -9
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d683d11a3567996b7eb36801581dfa374309207bda22d8bc7d7e4e7f94d1cf2f
|
|
4
|
+
data.tar.gz: 850af6daff806f5c56e4c5b9cd2294452360c891fca2690367111ff960c721fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c2272a6357f9fe281f36078facc23678c044b1942213bd4e881881c67fbd1ad2332aaf7f55d82cbe354ab558661b6e2fa4883044d7e07897508d6518f51b3a36
|
|
7
|
+
data.tar.gz: d4e99b1672e12be69e3c36b1b5e29b9a6b3ed1e68d17a7f308e8a33d6e7a41d5790c5b2ea552c796d335e0fba94ff540d3788389b91b252d81b141ba5e38b869
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,34 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.98.0] - 2026-10-21
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- A long question from the model gets room in the web stage: when its reply doesn't fit, the stage grows (up to 85%
|
|
16
|
+
of the panel) for the turn, the reply reads from its start in one scroll, and an "Answer ↓" pill leads to the card
|
|
17
|
+
while it is out of view. Approvals, the step limit and hooks' questions keep their layout.
|
|
18
|
+
- A new hook event, `:question_asked`, lets a plugin attach refs to the reply that asks a question or an approval
|
|
19
|
+
(display only, never in the prompt); the web pairs them in the asking text, on the card and in the history step.
|
|
20
|
+
A bundle manifest's hook can list `events: [...]` to run one hook for several events. See
|
|
21
|
+
[docs/hooks.md](docs/hooks.md).
|
|
22
|
+
- source-links 0.6.0 links the issue keys and paths in a waiting question's reply too, not only in answers.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- The web's rename ✎ moved from the info bar to the session's title: on the active card and next to the title in
|
|
27
|
+
the top bar, with the input over the title.
|
|
28
|
+
- A model server's 404 that lists its models puts `did you mean …?` ahead of the (cut) list, and names up to three
|
|
29
|
+
ids: every nearest typo on a tie, or the listed ids that contain the name asked for (`Qwen3.6-35B` →
|
|
30
|
+
`mlx-community--Qwen3.6-35B-A3B-4bit` or `…-8bit`).
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- A Gemma 4 step on the native loop no longer shows its tool call's body (`call:ask_user_question{…}`) as text in
|
|
35
|
+
the web history after a reload.
|
|
36
|
+
|
|
37
|
+
Update with `chi update` (the system bundle 0.98.0, source-links 0.6.0, the dev profile 0.7.1), then restart `chi web`.
|
|
38
|
+
|
|
11
39
|
## [0.97.0] - 2026-10-21
|
|
12
40
|
|
|
13
41
|
### Added
|
|
@@ -3727,7 +3755,8 @@ and long-lived sessions.
|
|
|
3727
3755
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3728
3756
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3729
3757
|
|
|
3730
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3758
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.98.0...HEAD
|
|
3759
|
+
[0.98.0]: https://github.com/dm1try/samagotchi/compare/v0.97.0...v0.98.0
|
|
3731
3760
|
[0.97.0]: https://github.com/dm1try/samagotchi/compare/v0.96.0...v0.97.0
|
|
3732
3761
|
[0.96.0]: https://github.com/dm1try/samagotchi/compare/v0.95.0...v0.96.0
|
|
3733
3762
|
[0.95.0]: https://github.com/dm1try/samagotchi/compare/v0.94.0...v0.95.0
|
data/docs/architecture.md
CHANGED
|
@@ -421,7 +421,14 @@ The page's frequent reads of `GET /api/sessions/:id` stay light too:
|
|
|
421
421
|
finishes the ended turn's own line by its `turn_id` (sent as `&turn_id=`, which also picks that
|
|
422
422
|
turn's answer). A tail read's timing is always the newest turn's; `recent=1` is accepted and not needed.
|
|
423
423
|
The page binds an answer's `display_refs` to its bubble (`answer_refs.js`: on a reload's render and on this
|
|
424
|
-
read's re-render) and pairs them with the bubble's links and text before the `path:line` scan. A
|
|
424
|
+
read's re-render) and pairs them with the bubble's links and text before the `path:line` scan. A question's
|
|
425
|
+
refs (`pending_question.refs`, `:question_asked`) pair the same way (`markdown_place.js` `linkRefsIn`/`pairRefsIn`):
|
|
426
|
+
`question_requested` (or a snapshot's pending question) keeps them on the asking step's gen
|
|
427
|
+
(`turn_model.js` `questionRefs`) before the card draws, so the stage's asking slot (`stage_slots.js`
|
|
428
|
+
`drawAsking(text, refs)`) pairs them on its raw draw and again after its markdown swap; the card pairs its
|
|
429
|
+
question text and its kept reply (`question_cards.js`); the step's closed `.gen-text` takes them
|
|
430
|
+
(`turn_view.js`; on a reload its `data-answer-at`, the message's `display_refs`) and `linkTextRefs` pairs it at
|
|
431
|
+
the turn's end and after a reload. A file ref outside the
|
|
425
432
|
session's folder is dropped, as a reload drops it: the folder `/tail` names, else (an older worker) the hub's
|
|
426
433
|
session list entry's, else (refs to check, no hub entry) the session file's; with no folder known at all, every
|
|
427
434
|
file ref is dropped rather than shown unchecked.
|
|
@@ -507,7 +514,8 @@ Bridge's stream above): the chat loop's thinking is the
|
|
|
507
514
|
server's `reasoning_content`; the raw-prompt loop splits each generation's
|
|
508
515
|
stream with a fresh `ThoughtStreamSplitter`, seeded with the prompt's prefill (Qwen's `<think>` and Gemma's
|
|
509
516
|
`<|channel>thought … <channel|>` are thinking, tool-call bodies are dropped
|
|
510
|
-
from `text
|
|
517
|
+
from `text:`; a saved step's text, as the web history and the attached terminal's join show it,
|
|
518
|
+
drops both too: `OutputFormatter.strip_markup`). The chat loop's model turns keep their `tool_calls` and tool results their `tool_call_id`,
|
|
511
519
|
so later requests and resumed sessions pair them, and their reasoning (`thinking:`), which goes back as
|
|
512
520
|
`reasoning_content` on the running turn's replies (`reasoning_history`; a step-limit Continue's
|
|
513
521
|
turn goes on from the run it resumes, `reasoning_since:`). It has its own system prompt
|
data/docs/bundle-authoring.md
CHANGED
|
@@ -51,7 +51,8 @@ plugin:
|
|
|
51
51
|
sha256: sha256:<hex> # `chi bundle check --fix` fills it; never compute it by hand
|
|
52
52
|
# files: # memories it installs (top-level *.md); without files: every *.md but README & co.
|
|
53
53
|
# my_notes.md: sha256:<hex>
|
|
54
|
-
# hooks: # hooks/*.rb files (a plugin's chi.on is usually simpler)
|
|
54
|
+
# hooks: # hooks/*.rb files (a plugin's chi.on is usually simpler), each with event: NAME
|
|
55
|
+
# # or events: [a, b] (one instance for both; chi newer than 0.97.0)
|
|
55
56
|
# needs: [gh] # outside commands it relies on
|
|
56
57
|
```
|
|
57
58
|
|
|
@@ -64,7 +65,7 @@ time. `chi bundle check --set KEY=VALUE` gives the sandboxed load a setting.
|
|
|
64
65
|
## What a hook sees
|
|
65
66
|
|
|
66
67
|
`chi.on(event) { |event, ctx| … }` gets a Hash. The events, in a turn's order: `session_start`, `before_turn`,
|
|
67
|
-
`before_generation`, `generation_progress`, `after_generation`, `before_tool_call`, `after_tool_call`,
|
|
68
|
+
`before_generation`, `generation_progress`, `after_generation`, `before_tool_call`, `question_asked`, `after_tool_call`,
|
|
68
69
|
`after_turn`, `session_end` (the fields of each: [hooks.md, Hook Events](hooks.md#hook-events)). A symbol not in
|
|
69
70
|
that list is never fired (`check` warns). The two a bundle wants most, for an `execute`:
|
|
70
71
|
|
|
@@ -116,7 +117,8 @@ end
|
|
|
116
117
|
|
|
117
118
|
`bundle.tool_call` runs guarded hooks; `bundle.tool` calls a registered tool. `bundle.steers`, `cards`, `stops` and
|
|
118
119
|
`questions` record effects; `bundle.answer` / `model_answers` provide `ask_user` / `ask_model` answers.
|
|
119
|
-
`bundle.http.stub` stubs `ctx.http`; `
|
|
120
|
+
`bundle.http.stub` stubs `ctx.http`; `bundle.question_asked(text, question:)` fires `:question_asked` and returns
|
|
121
|
+
the refs; `sandbox.config`, `sandbox.fake_executable` and `bundle.install` cover setup.
|
|
120
122
|
The full list is in [plugins.md, Testing a bundle](plugins.md#testing-a-bundle-experimental). The kit is
|
|
121
123
|
experimental (it may change before chi 1.0). If a test needs a fake of chi's own (a stub of a `Samagotchi::`
|
|
122
124
|
class, a hand-built `ctx`), that is a gap in the kit: say so to the user rather than reaching into chi.
|
data/docs/cli.md
CHANGED
|
@@ -659,8 +659,8 @@ the first message: …)`); `/title TEXT` names the session (whitespace collapsed
|
|
|
659
659
|
cut at 80 characters), `/title --clear` drops the name (anything after `--clear`
|
|
660
660
|
is refused with the usage line). It is saved at once,
|
|
661
661
|
mid-turn too, and the lists, cards, top bar and tab show it in place of the first
|
|
662
|
-
message; a continue's next link keeps it.
|
|
663
|
-
[Session titles](sessions.md#session-titles).
|
|
662
|
+
message; a continue's next link keeps it. In the web, `✎` next to the session's
|
|
663
|
+
title (its strip card, or the top bar) does it. See [Session titles](sessions.md#session-titles).
|
|
664
664
|
|
|
665
665
|
A `/thinking`, `/reasoning-history` or `/toolkit` change that restarts the prompt cache
|
|
666
666
|
asks first: the reply says what it costs, changes nothing, and names the line
|
|
@@ -895,7 +895,12 @@ nudged the model") show ahead of those calls for about 4 s, as their rows
|
|
|
895
895
|
wait in the closed block. A card that needs you (a question, an approval, check-in)
|
|
896
896
|
sits in the stage too. While a question or approval waits, the stage shows
|
|
897
897
|
the whole text of the reply that asked it (in place of its newest
|
|
898
|
-
sentence) above the card, until you answer.
|
|
898
|
+
sentence) above the card, until you answer. A long reply that asks the
|
|
899
|
+
model's own question (one that doesn't fit that slot) gets more room: the
|
|
900
|
+
stage grows to about 85% of the page for the rest of the turn (never above
|
|
901
|
+
the session panel), the reply reads from its start in the stage's one
|
|
902
|
+
scroll, and while the card is out of view a `❓ Answer ↓` pill at the
|
|
903
|
+
stage's bottom edge takes you to it. Once answered, the model's
|
|
899
904
|
question or its own approval keeps that reply (a delegate's relayed
|
|
900
905
|
approval, a hook's question and the step limit don't): two muted lines
|
|
901
906
|
under its `question → answer` line, the whole text when you open the card,
|
data/docs/hooks.md
CHANGED
|
@@ -56,6 +56,7 @@ The plugin class must respond to `#call(event)` — duck-typed, no base class re
|
|
|
56
56
|
| `: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) }` |
|
|
57
57
|
| `: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) }` |
|
|
58
58
|
| `:before_tool_call` | Before tool dispatch (and before `tool_call_started`) | `{ type: :before_tool_call, iteration: N, call: {...}, params: "...", guardrail: Verdict, context: {...}, targets: {...}, blocked: false, block_reason: nil }` |
|
|
59
|
+
| `:question_asked` | A question of the model's own (`ask_user_question`) or an approval of its tool call is about to open, on the turn's thread (not a hook's or plugin's `ask_user`, a relayed delegate approval, the step-limit question, a `--non-interactive` run or a stored approval: none of those fire it) | `{ type: :question_asked, kind: "question" \| "approval", text: "..." (the reply that asks, as the asking slot shows it: no thoughts, no tool-call markup), question: {...} (the question's fields, frozen: question/options/header…, an approval's approval: facts too; no id yet), refs: (see [Refs on a question](#refs-on-a-question-display-only)) }` |
|
|
59
60
|
| `:after_tool_call` | After tool execution | `{ type: :after_tool_call, iteration: N, tool: "read", output: "..." (the text the model gets: capped at max_tool_output_chars; a cut one has a [cut: …] line, at its end, or for an execute result between its head and its tail, the whole output saved in the session's tool_output/ folder), status: "ok" \| "error" \| "blocked" \| "stopped", call: {...} (the call that ran, as the gate left it), elapsed_ms: N (the tool's own run, without an approval wait; nil for a denied call), result: "..." (the tool's own text: no "[read]" lead, no "ran as:" line, never cut) }`; `stopped` is a `task_wait` the turn's Stop ended (the task runs on) or whose task was stopped. Match on `result:`, not `output:`: it is what the tool said (`Error: command timed out after 120s` for an execute killed at its limit in kill mode, `Samagotchi::Tools::Execute::TIMEOUT_PREFIX`; `Still running at the 120s limit: …` for one handed over as a task, `Samagotchi::Tools::Execute::HANDED_OVER`) |
|
|
60
61
|
| `:session_end` | After each turn `:after_turn` fires for, after it (turn-level lifecycle) | `{ type: :session_end, session_id: "..." }` |
|
|
61
62
|
|
|
@@ -366,6 +367,58 @@ end
|
|
|
366
367
|
|
|
367
368
|
A plugin gets the same from `chi.on(:after_turn) { |event, ctx| event[:refs].call(…) }`.
|
|
368
369
|
|
|
370
|
+
## Refs on a question (display only)
|
|
371
|
+
|
|
372
|
+
`:question_asked` carries `refs:` too, for the reply that asks the question: a
|
|
373
|
+
ticket the model names while it asks is a ref while the question waits, when
|
|
374
|
+
the user needs it.
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
class JiraQuestionRefs
|
|
378
|
+
def call(event)
|
|
379
|
+
return unless event[:type] == :question_asked
|
|
380
|
+
|
|
381
|
+
event[:text].scan(/\b[A-Z][A-Z0-9]+-\d+\b/).uniq.each do |key|
|
|
382
|
+
event[:refs].call(match: key, kind: "issue", target: { id: key }, label: key,
|
|
383
|
+
actions: [{ id: "open", label: "Open in Jira", url: "https://jira.example.com/browse/#{key}" }])
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
- The call, its return values, the checks and the `refs_rejected` log are
|
|
390
|
+
[Refs on the answer](#refs-on-the-answer-display-only)'s, with lower caps:
|
|
391
|
+
50 refs and 32 KiB per step. `nil`: the turn has no model step to attach to,
|
|
392
|
+
or the call came after the hooks ran.
|
|
393
|
+
- They are kept as `display_refs` on the step's model message (the one that
|
|
394
|
+
asks; in the session file after the turn, a cancelled one too) and go on the
|
|
395
|
+
pending question as its `refs` (the `question_requested` event, the session
|
|
396
|
+
file while it waits, the saved question card). A second question in the same
|
|
397
|
+
step adds to the step's list, and its pending question has the whole list.
|
|
398
|
+
The model never sees them.
|
|
399
|
+
- The web checks them again wherever a saved one reaches it (the pending
|
|
400
|
+
question, a saved card, the history), as an answer's, and pairs them as an
|
|
401
|
+
answer's (the three passes above) in: the stage's asking slot while the
|
|
402
|
+
question waits (its raw text, then again once its markdown is in), the
|
|
403
|
+
card's question text, the answered card's kept reply (raw, then its
|
|
404
|
+
markdown when opened; never its one-line summary copy), and that step's
|
|
405
|
+
text in the history, live (at the turn's end) and after a reload. A ref
|
|
406
|
+
whose `match` is only in the question's text pairs there alone; the step's
|
|
407
|
+
text drops it. **Terminals** ignore them.
|
|
408
|
+
- **Its time is the question's wait**: the hooks run before the question
|
|
409
|
+
opens, so a slow one delays it (and counts in `ask_user_question`'s
|
|
410
|
+
`elapsed_ms` and an approval's `waited_ms`). A hook that raises is logged and
|
|
411
|
+
the question opens with the other hooks' refs. With no hook on the event
|
|
412
|
+
nothing is built.
|
|
413
|
+
- Don't ask from it: `event[:ask_user]` (and `ctx.ask_user` in a `chi.on`
|
|
414
|
+
block) returns nil at once here.
|
|
415
|
+
- Needs chi newer than 0.97.0: set `requires_chi`.
|
|
416
|
+
|
|
417
|
+
A plugin gets the same from `chi.on(:question_asked) { |event, ctx| event[:refs].call(…) }`. The bundle test
|
|
418
|
+
kit fires it as chi does: `bundle.question_asked(text, kind:, question:)` returns the refs the step gets
|
|
419
|
+
([plugins.md](plugins.md#testing-a-bundle-experimental)). The shipped source-links bundle subscribes (its
|
|
420
|
+
sources' refs in the reply that asks, and in the question's text and options).
|
|
421
|
+
|
|
369
422
|
## Settings
|
|
370
423
|
|
|
371
424
|
A hook class whose `initialize` takes an argument gets its settings: **one
|
|
@@ -599,6 +652,12 @@ Notes:
|
|
|
599
652
|
|
|
600
653
|
- Hook key = basename (flat under `hooks/`). No subdirs in v1.
|
|
601
654
|
- `event` is required for auto-registration; a hook with no event is skipped.
|
|
655
|
+
- `events: [after_turn, question_asked]` (a list, in place of `event:`) registers the hook for each event it
|
|
656
|
+
names: **one** instance serves them all (its state is shared), and `#call` tells them apart by
|
|
657
|
+
`event[:type]`. `event:` and `events:` together, or an `events:` that isn't a non-empty list of event names,
|
|
658
|
+
is refused at install (`hooks: <file>: …`). `on_error: fail_closed` applies to its `before_tool_call`
|
|
659
|
+
registration only. Needs chi newer than 0.97.0 (an older chi installs the hook with no event and never loads
|
|
660
|
+
it): set `requires_chi`.
|
|
602
661
|
- `sha256` is integrity (not authenticity). No signing in v1. Install records the sha256 of the copied file; at `Engine.new` a hook whose file differs is not loaded (reinstall the bundle after editing one by hand).
|
|
603
662
|
- Hook code is the bundle author's source of truth: on upgrade, hooks are overwritten; if the installed file was locally modified, a warning is emitted (`was locally modified; overwriting`).
|
|
604
663
|
- `on_error` defaults to `skip` for every event, `:before_tool_call` included: a raising hook is silent and the call goes ahead. `log` warns. `fail_closed` (only on `:before_tool_call`; on any other event it acts as `skip`) denies the call the hook raised on. Choose `fail_closed` for a hook that is a guardrail, whose failure should stop tool calls rather than let them through.
|
data/docs/internals/bundles.md
CHANGED
|
@@ -37,7 +37,7 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
37
37
|
| `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps) |
|
|
38
38
|
| `Merger` | 3-way classification per file: `install`, `fast_forward`, `keep`, `noop`, `conflict` |
|
|
39
39
|
| `Provenance` | the `.bundles/<name>/` dir: `#write` the record + bases, `#record` → `InstalledBundle`, `.each_installed`, `.claimants` (which bundles own a file) |
|
|
40
|
-
| `InstalledBundle`, `BundleHook`, `PluginRef` | value objects for the record; tolerant of older records (missing keys read as empty) |
|
|
40
|
+
| `InstalledBundle`, `BundleHook`, `PluginRef` | value objects for the record; tolerant of older records (missing keys read as empty). `BundleHook#events` is the manifest's `event:` or its `events:` list (written back as `event:` for one, as an older chi reads it); `Hooks::BundleLoader#load_hook_file` builds the hook once and registers it under each |
|
|
41
41
|
| `Builder` | `chi bundle build`: memories (+ the installed bundle's hooks, rules, plugin, scripts) → zip/tar/dir |
|
|
42
42
|
| `Profile` | meta bundles (`includes:`): install the members, record which; `InstallResult#kept_own` names a new member's memory files the user already had, with other bytes (`kept_own_line`, printed by `chi bundle`, `chi update` and `chi bootstrap`); a dry run runs each member's Installer as a dry run too, so it names them as well |
|
|
43
43
|
| `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` |
|
data/docs/internals/engine.md
CHANGED
|
@@ -594,6 +594,23 @@ logged). Both are display only: `AnswerDisplay::DISPLAY_ONLY` lists `display`, `
|
|
|
594
594
|
after_generation), while `KernelLoop#sanitize_history` and `ChatLoop#plain` copy them so the stored conversation keeps
|
|
595
595
|
them across turns. `Web::App#messages_for_display` sends `display_refs` re-validated (`AnswerRefs.for_display`).
|
|
596
596
|
|
|
597
|
+
`:question_asked` (`QuestionAsked`, `question_asked.rb`) fires from the two Engine seams where a question of the
|
|
598
|
+
model's opens, never from `QuestionDesk`: `Engine#request_question` (the kernel's `question_handler`, after
|
|
599
|
+
`AskUserQuestion.validate`; not with `interface == :non_interactive`) and the guardrail wiring's `ask:` lambda
|
|
600
|
+
(`GuardrailWiring#request_approval`, after a stored approval and the non-interactive deny). So `ask_for_hook`, a
|
|
601
|
+
relayed approval (`EnginePeers#open_question` → `Engine#open_question`) and `post` (the step-limit question) never
|
|
602
|
+
fire it. `QuestionAsked.fire` is a no-op without a subscriber (`Registry#any?`); else it finds the live
|
|
603
|
+
conversation's last `role: "model"` entry (`Engine#live_conversation`, one lookup for both loops: a ChatLoop batch may
|
|
604
|
+
end in a tool result), builds an `AnswerRefs` on that step alone with the question caps (50 / 32 KiB), fires with the
|
|
605
|
+
step's text (`Engine#question_step_text`: the parser's thought and tool-call strip, then
|
|
606
|
+
`OutputFormatter.strip_markup`), a frozen copy of the fields and a no-op `ask_user`, then sets `display_refs` on
|
|
607
|
+
the step **in place** under the event lock (the loops and the forget layer hold entries by identity) and returns the
|
|
608
|
+
fields with `refs:`. `QuestionDesk#request` forwards `refs` past its slice; `PendingQuestion#refs` carries them to
|
|
609
|
+
`:question_requested`, the session file and the cards. `Web::App#with_question_refs` re-validates them (the pending
|
|
610
|
+
question, `cards_for_display`) with the same caps; the page pairs them in the asking slot, on the card and on the
|
|
611
|
+
step's text ([architecture.md](../architecture.md), the `display_refs` paragraph). The kit's
|
|
612
|
+
`Testing::Bundle#question_asked` calls `QuestionAsked.fire` with a one-step conversation.
|
|
613
|
+
|
|
597
614
|
### Hook lifecycle
|
|
598
615
|
|
|
599
616
|
- **Bundle hooks** (`Registry#register_bundle`) and **config hooks** (`register_persistent`, from config.yml)
|
|
@@ -756,6 +773,7 @@ Paths are under `lib/samagotchi/`.
|
|
|
756
773
|
| `toolkit_binding.rb` | `ToolkitBinding`: the Engine's toolkit seam (the kits, `#explained`, `ctx.use_toolkit`, the turn-start `#apply!` behind `Engine#apply_toolkit!`) |
|
|
757
774
|
| `hooks/registry.rb` | Hook registry, fire order, `Hooks::Runtime` |
|
|
758
775
|
| `hook_routes.rb` | `HookRoutes`: the Engine's routes for a hook's or plugin's notice (`:hook_notice`, `fallback_for:`), question, stop and steer, and the guardrails' after-load line |
|
|
776
|
+
| `question_asked.rb` | `QuestionAsked`: the `:question_asked` fire, the asking step's refs (`display_refs` in place, the pending question's `refs`) |
|
|
759
777
|
| `question_desk.rb`, `pending_question.rb`, `question_answer.rb` | `QuestionDesk`: the one open question (ask, post, answer, cancel, relay mark); `PendingQuestion` and `QuestionAnswer`, its values (Hashes only in events, Bridge JSON, the session file and the hook/plugin `ask_user` result) |
|
|
760
778
|
| `hook_question.rb` | `HookQuestion`: a hook's or plugin's `ask_user` question as one value, from the runtime/host edge to `QuestionDesk#ask_for_hook` |
|
|
761
779
|
| `event_routes.rb` | `EventRoutes`: where a card (`Engine#show_card`), a notice (`HookRoutes#notify`) and a guardrails line (`#guardrail_notify`, turn or between turns only) go: held while the plugins load, an anytime command's, an init task's, a running turn's event, else announced or held for `holding_announcements` |
|
data/docs/plugins.md
CHANGED
|
@@ -433,7 +433,9 @@ This is a bundle hook, the same as a `hooks/*.rb` file. See
|
|
|
433
433
|
[hooks.md](hooks.md#hook-events) for the events; on `:after_turn`,
|
|
434
434
|
`event[:present]` sets how the answer is shown in the web
|
|
435
435
|
([Presenting the answer](hooks.md#presenting-the-answer-display-only)) and `event[:refs]`
|
|
436
|
-
attaches refs to it ([Refs on the answer](hooks.md#refs-on-the-answer-display-only))
|
|
436
|
+
attaches refs to it ([Refs on the answer](hooks.md#refs-on-the-answer-display-only)); on
|
|
437
|
+
`:question_asked`, `event[:refs]` attaches refs to the reply that asks a question
|
|
438
|
+
([Refs on a question](hooks.md#refs-on-a-question-display-only)). Its label is
|
|
437
439
|
`plugin.rb (bundle my-bundle)`. The block may take only the event. If it
|
|
438
440
|
raises, the error is logged and the hook is skipped.
|
|
439
441
|
|
|
@@ -1159,6 +1161,7 @@ require "samagotchi/testing"
|
|
|
1159
1161
|
bundle = Samagotchi::Testing::Bundle.load("path/to/my-bundle", settings: { "every" => 3 },
|
|
1160
1162
|
host: { session_id: "s1", cwd: Dir.pwd })
|
|
1161
1163
|
bundle.fire(:after_turn, status: "completed") # → the event, after the hooks
|
|
1164
|
+
bundle.question_asked("Fix JIRA-1?", question: { question: "Go?" }) # → the refs, as chi's seams fire it
|
|
1162
1165
|
event = bundle.before_tool_call_event({ name: "read", path: "a.txt" }, params: "read a.txt")
|
|
1163
1166
|
bundle.command("/mine stop") # → what the command returned
|
|
1164
1167
|
bundle.tool("my_tool", path: "a.txt") # → the text the model reads
|
|
@@ -1212,6 +1215,10 @@ bundle.shutdown
|
|
|
1212
1215
|
lays them over `settings:` through chi's own `Context#settings`; the plugin's `initialize(settings)` gets
|
|
1213
1216
|
`settings:` only. `bundle.set(session_settings:)` moves the session to a new id (`test-session-2`, or the
|
|
1214
1217
|
`session_id:` given beside it): chi never changes them within a session, and builds `ctx.settings` once per id.
|
|
1218
|
+
- **`question_asked(text, kind: "question", question: {}, cwd: nil)`** fires `:question_asked` through
|
|
1219
|
+
`QuestionAsked.fire`, as chi's two seams do: a step whose text is `text` asks a question (or `kind: "approval"`)
|
|
1220
|
+
with the `question` fields, and the refs the hooks attach come back checked (the question caps, a file ref under
|
|
1221
|
+
`cwd:`, default the session's folder), as the step and its pending question keep them; `[]` for none.
|
|
1215
1222
|
- **A hook that raises fails the call** (`Testing::HookFailed`, with the hook's error) where chi would log it and
|
|
1216
1223
|
go on; `bundle.lenient { … }` collects them in `bundle.errors` instead.
|
|
1217
1224
|
- **Sandbox.** A test runs with temp config, state, data and cache homes (config.yml, memories, `ctx.data_dir`, the
|
data/docs/sessions.md
CHANGED
|
@@ -221,7 +221,7 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
|
|
|
221
221
|
|
|
222
222
|
A session is named by its first message until you give it a title: `/title Coordinator` (in the REPL, an attached terminal, the web composer or `chi send -m "/title Coordinator" ID`; a parent agent may name its delegates the same way). The title is saved in the session file (`title`), whitespace collapsed and cut at 80 characters, and runs mid-turn too, at once. `/title` alone shows it (`title: none (shows the first message: hey chi …)` without one); `/title --clear` drops it, and the first message names the session again. A session whose first line was the command itself (`chi send --new -m "/title X"`) takes its preview from its first real message.
|
|
223
223
|
|
|
224
|
-
The title names the session wherever its first message did: `chi sessions list` (plain, `--live`, tsv and json's `desc`, before the recap; json has `title` too), `chi sessions delete`'s line, the `list_sessions` tool, `chi broadcast`, the desktop helper, a parent's children (the coordinator bundle's `/children` card, `ctx.sessions.children`; `ctx.sessions.read` too), and in the web the cards, the families and chain rows, the top bar and the browser tab, the notifications and the search; the terminal's status row too (the REPL's and an attached one's, first, see [Status Line](cli.md#status-line)).
|
|
224
|
+
The title names the session wherever its first message did: `chi sessions list` (plain, `--live`, tsv and json's `desc`, before the recap; json has `title` too), `chi sessions delete`'s line, the `list_sessions` tool, `chi broadcast`, the desktop helper, a parent's children (the coordinator bundle's `/children` card, `ctx.sessions.children`; `ctx.sessions.read` too), and in the web the cards, the families and chain rows, the top bar and the browser tab, the notifications and the search; the terminal's status row too (the REPL's and an attached one's, first, see [Status Line](cli.md#status-line)). In the web, `✎` (Rename this session) sits next to the session's title, on its active strip card or after the top bar's title: a small input over the title, prefilled with it; Enter or a click elsewhere sends `/title TEXT` when it changed (empty: `/title --clear`), Esc closes it unsent, and so does the title leaving the screen (All sessions opened, the card swiped out of a phone's strip). A session a chi REPL owns has no `✎`, nor one that shows no title yet (`/title` still works from the composer). A continue's new link keeps the title (see [Session chains](#session-chains)). An older `chi web` (or any chi from before the field) that saves a session drops its title: restart `chi web` after upgrading.
|
|
225
225
|
|
|
226
226
|
## Session chains
|
|
227
227
|
|
|
@@ -52,7 +52,9 @@ module Samagotchi
|
|
|
52
52
|
#
|
|
53
53
|
# The target is AnswerDisplay's: the stored conversation's last message
|
|
54
54
|
# when it is the model's. The list starts from the refs it already has
|
|
55
|
-
# (re-validated), so a later hook run never loses them.
|
|
55
|
+
# (re-validated), so a later hook run never loses them. QuestionAsked
|
|
56
|
+
# uses it too, on the step that asks a question (+messages+ that one
|
|
57
|
+
# step, lower caps).
|
|
56
58
|
class AnswerRefs
|
|
57
59
|
KEY = :display_refs
|
|
58
60
|
MAX_REFS = 200
|
|
@@ -63,12 +65,13 @@ module Samagotchi
|
|
|
63
65
|
# rebuilt and checked again, the caps applied; what isn't valid any
|
|
64
66
|
# more is dropped. Never raises: a reload must show the conversation.
|
|
65
67
|
# A relative file path is dropped (it was stored absolute), and with
|
|
66
|
-
# +cwd+ (the session's folder) a file outside it.
|
|
68
|
+
# +cwd+ (the session's folder) a file outside it. +max_refs+ and
|
|
69
|
+
# +max_bytes+: the caps (a question's are lower, QuestionAsked).
|
|
67
70
|
# @return [Array<Hash>] the refs to send ({match:, kind:, target:, …})
|
|
68
|
-
def self.for_display(raw, cwd: nil)
|
|
71
|
+
def self.for_display(raw, cwd: nil, max_refs: MAX_REFS, max_bytes: MAX_BYTES)
|
|
69
72
|
return [] if raw.nil?
|
|
70
73
|
|
|
71
|
-
kept = new([{ role: "model", KEY => raw }], cwd: cwd).to_a
|
|
74
|
+
kept = new([{ role: "model", KEY => raw }], cwd: cwd, max_refs: max_refs, max_bytes: max_bytes).to_a
|
|
72
75
|
dropped = raw.is_a?(Array) ? raw.length - kept.length : 1
|
|
73
76
|
Log.debug(:web, "display_refs_dropped", count: dropped) if dropped.positive?
|
|
74
77
|
kept
|
|
@@ -81,10 +84,14 @@ module Samagotchi
|
|
|
81
84
|
|
|
82
85
|
# @param messages [Array<Hash>] the conversation the turn stored
|
|
83
86
|
# @param cwd [String, nil] the session's folder, for relative file paths
|
|
84
|
-
|
|
87
|
+
# @param max_refs [Integer] the list's cap in refs
|
|
88
|
+
# @param max_bytes [Integer] the list's cap in JSON bytes
|
|
89
|
+
def initialize(messages, cwd:, max_refs: MAX_REFS, max_bytes: MAX_BYTES)
|
|
85
90
|
last = Array(messages).last
|
|
86
91
|
@target = last if last.is_a?(Hash) && (last[:role] || last["role"]).to_s == "model"
|
|
87
92
|
@roots = RowRefs.roots(cwd)
|
|
93
|
+
@max_refs = max_refs
|
|
94
|
+
@max_bytes = max_bytes
|
|
88
95
|
@refs = []
|
|
89
96
|
@seen = {}
|
|
90
97
|
@bytes = 2 # the list's []
|
|
@@ -143,10 +150,10 @@ module Samagotchi
|
|
|
143
150
|
|
|
144
151
|
entry = AnswerRef.from(value, roots: @roots).to_h
|
|
145
152
|
return true if @seen.key?(entry)
|
|
146
|
-
return over_cap(hook, "#{
|
|
153
|
+
return over_cap(hook, "#{@max_refs} refs") if @refs.length >= @max_refs
|
|
147
154
|
|
|
148
155
|
size = JSON.generate(entry).bytesize + (@refs.empty? ? 0 : 1)
|
|
149
|
-
return over_cap(hook, "#{
|
|
156
|
+
return over_cap(hook, "#{@max_bytes / 1024} KiB") if @bytes + size > @max_bytes
|
|
150
157
|
|
|
151
158
|
push(entry, size)
|
|
152
159
|
@added = true
|
|
@@ -160,10 +167,10 @@ module Samagotchi
|
|
|
160
167
|
# @return [Boolean] kept
|
|
161
168
|
def keep(raw)
|
|
162
169
|
entry = AnswerRef.from(raw, roots: @roots, relative: false).to_h
|
|
163
|
-
return false if @seen.key?(entry) || @refs.length >=
|
|
170
|
+
return false if @seen.key?(entry) || @refs.length >= @max_refs
|
|
164
171
|
|
|
165
172
|
size = JSON.generate(entry).bytesize + (@refs.empty? ? 0 : 1)
|
|
166
|
-
return false if @bytes + size >
|
|
173
|
+
return false if @bytes + size > @max_bytes
|
|
167
174
|
|
|
168
175
|
push(entry, size)
|
|
169
176
|
true
|
|
@@ -476,7 +476,7 @@ module Samagotchi
|
|
|
476
476
|
hooks.each do |hk, hook|
|
|
477
477
|
hook_path = File.join(Samagotchi::MemoryBundle::Provenance.new(name: name).hooks_dir, hk)
|
|
478
478
|
exists = File.exist?(hook_path) ? "ok" : "missing"
|
|
479
|
-
@stdout.puts " #{hk}:
|
|
479
|
+
@stdout.puts " #{hk}: #{hook.events_field} on_error=#{hook.on_error} priority=#{hook.priority} [#{exists}]"
|
|
480
480
|
end
|
|
481
481
|
@stdout.puts " requires_chi: #{st[:provenance].requires_chi}" if st[:hooks_requires_failure]
|
|
482
482
|
@stdout.puts " not loaded: #{st[:hooks_requires_failure]}" if st[:hooks_requires_failure]
|
|
@@ -658,7 +658,7 @@ module Samagotchi
|
|
|
658
658
|
end
|
|
659
659
|
|
|
660
660
|
def hook_metadata_line(hook)
|
|
661
|
-
"--- metadata:
|
|
661
|
+
"--- metadata: #{hook.events_field} on_error=#{hook.on_error} priority=#{hook.priority} sha256=#{hook.sha256}"
|
|
662
662
|
end
|
|
663
663
|
|
|
664
664
|
def build(rest)
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
require "open3"
|
|
5
5
|
require "samagotchi/ref"
|
|
6
6
|
|
|
7
|
-
# An after_turn hook that links the source refs the
|
|
8
|
-
# mentions — a JIRA ticket, a GitHub issue, an internal wiki page. In the
|
|
7
|
+
# An after_turn (and question_asked) hook that links the source refs the
|
|
8
|
+
# model's answer mentions — a JIRA ticket, a GitHub issue, an internal wiki page. In the
|
|
9
9
|
# web, each ref in the answer becomes a link (`[JIRA-123](https://…)`,
|
|
10
10
|
# through event[:present]: display only, the model's text stays as it was,
|
|
11
11
|
# and it survives a reload). Every UI also gets one line right after the
|
|
@@ -58,6 +58,14 @@ require "samagotchi/ref"
|
|
|
58
58
|
# action, "Open in <name>", the ref's URL. A URL that isn't http(s) stays a
|
|
59
59
|
# markdown link only. The web's ref menu shows the action.
|
|
60
60
|
#
|
|
61
|
+
# The hook is on :question_asked too (the manifest's events:, chi newer than
|
|
62
|
+
# 0.97.0): while a question of the model's (or an approval of its call)
|
|
63
|
+
# waits, the refs in the reply that asks it, and in the question's text and
|
|
64
|
+
# options, are refs on that reply, the same as an answer's. The web pairs
|
|
65
|
+
# them in the stage's asking slot, on the question's card and on that step
|
|
66
|
+
# in the history (a ref only in the question's text pairs there alone). No
|
|
67
|
+
# links in the text and no note line for it: the turn's end has those.
|
|
68
|
+
#
|
|
61
69
|
# The note is not stored in the conversation: it is an event, replayed by a
|
|
62
70
|
# UI with the session's cards (a reload keeps it, after the worker stopped
|
|
63
71
|
# too: cards.json). The links are stored as the answer's display. A ref in
|
|
@@ -120,10 +128,20 @@ class SourceLinks
|
|
|
120
128
|
end
|
|
121
129
|
|
|
122
130
|
def call(event)
|
|
123
|
-
return unless event.is_a?(Hash)
|
|
124
|
-
return unless event[:status].to_s == "completed"
|
|
131
|
+
return unless event.is_a?(Hash)
|
|
125
132
|
return if @sources.empty?
|
|
126
133
|
|
|
134
|
+
case event[:type]
|
|
135
|
+
when :after_turn then after_turn(event)
|
|
136
|
+
when :question_asked then question_asked(event)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def after_turn(event)
|
|
143
|
+
return unless event[:status].to_s == "completed"
|
|
144
|
+
|
|
127
145
|
text = last_model_text(event[:messages])
|
|
128
146
|
return if text.nil? || text.empty?
|
|
129
147
|
|
|
@@ -141,7 +159,23 @@ class SourceLinks
|
|
|
141
159
|
event[:notify]&.call(line(found), level: :info, fallback_for: covered ? :display : nil)
|
|
142
160
|
end
|
|
143
161
|
|
|
144
|
-
|
|
162
|
+
# A question of the model's (or an approval of its call) is about to
|
|
163
|
+
# open: the refs in the reply that asks it and in the question's own text
|
|
164
|
+
# and options, each scanned on its own (a code span never runs from one
|
|
165
|
+
# into the next), as refs on that reply. No present (a mid-turn text has
|
|
166
|
+
# no display) and no note line (the after-turn one stays the terminals').
|
|
167
|
+
def question_asked(event)
|
|
168
|
+
refs = event[:refs]
|
|
169
|
+
return unless refs
|
|
170
|
+
|
|
171
|
+
question = event[:question].is_a?(Hash) ? event[:question] : {}
|
|
172
|
+
field = ->(key) { question.key?(key) ? question[key] : question[key.to_s] }
|
|
173
|
+
parts = [event[:text], field.call(:question), *Array(field.call(:options))]
|
|
174
|
+
parts.each do |part|
|
|
175
|
+
text = part.is_a?(String) ? part : ""
|
|
176
|
+
add_refs(refs, occurrences(text[0, MAX_SCAN])) unless text.empty?
|
|
177
|
+
end
|
|
178
|
+
end
|
|
145
179
|
|
|
146
180
|
# Links the refs in the answer as shown: the model's text unless an
|
|
147
181
|
# earlier hook changed it (then its offsets differ, so it is scanned
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: source-links
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.6.0
|
|
4
4
|
scope: system
|
|
5
|
-
description: Links source refs (JIRA tickets, GitHub issues, …) in the model's answer — inline links in the web, and a one-line note after the turn (note false turns it off); sources are configured patterns
|
|
5
|
+
description: Links source refs (JIRA tickets, GitHub issues, …) in the model's answer — inline links in the web, refs in a waiting question's reply, and a one-line note after the turn (note false turns it off); sources are configured patterns
|
|
6
6
|
trust_level: reviewed
|
|
7
|
-
requires_chi: ">= 0.
|
|
7
|
+
requires_chi: ">= 0.98.0"
|
|
8
8
|
files:
|
|
9
9
|
source_links.md: sha256:ace01a0f7c308a0ae54ece714c31c0e6d7a49c372d98111030e6d268320bed3a
|
|
10
10
|
hooks:
|
|
11
11
|
source_links.rb:
|
|
12
|
-
sha256: sha256:
|
|
13
|
-
|
|
12
|
+
sha256: sha256:84c0bef064fb1ea76749a61e758eba612662aaf0f05f18a18ab4228497078fa2
|
|
13
|
+
events: [after_turn, question_asked]
|
|
14
14
|
on_error: log
|
|
15
15
|
priority: 90
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -77,6 +77,7 @@ require_relative "answer_display"
|
|
|
77
77
|
require_relative "answer_refs"
|
|
78
78
|
require_relative "edit_preview"
|
|
79
79
|
require_relative "question_desk"
|
|
80
|
+
require_relative "question_asked"
|
|
80
81
|
require_relative "child_reports"
|
|
81
82
|
require_relative "relay_desk"
|
|
82
83
|
require_relative "guardrail_wiring"
|
|
@@ -364,7 +365,8 @@ module Samagotchi
|
|
|
364
365
|
model_key: -> { @model_key },
|
|
365
366
|
model_name: -> { guardrail_model_name },
|
|
366
367
|
cancelled: -> { !!active_cancel_controller&.cancelled? },
|
|
367
|
-
|
|
368
|
+
# An approval of the model's call: :question_asked first (QuestionAsked).
|
|
369
|
+
ask: ->(fields) { open_question(question_asked(QuestionAsked::KIND_APPROVAL, fields)) },
|
|
368
370
|
notify: ->(message) { guardrail_notify(message) }
|
|
369
371
|
)
|
|
370
372
|
end
|
|
@@ -1576,13 +1578,35 @@ module Samagotchi
|
|
|
1576
1578
|
|
|
1577
1579
|
# Ask the model's question (ask_user_question): the kernel's
|
|
1578
1580
|
# question_handler, called on the turn thread with the payload
|
|
1579
|
-
# Tools::AskUserQuestion.validate made (QuestionDesk#request)
|
|
1581
|
+
# Tools::AskUserQuestion.validate made (QuestionDesk#request), after
|
|
1582
|
+
# the :question_asked hooks (QuestionAsked) gave the step its refs; a
|
|
1583
|
+
# --non-interactive run has no one to ask, so they don't fire there.
|
|
1580
1584
|
# @param payload [Hash] {question:, options:, header:, multi_select:, allow_freeform:}
|
|
1581
1585
|
# @return [String] normalized answer JSON
|
|
1582
1586
|
def request_question(payload)
|
|
1587
|
+
payload = question_asked(QuestionAsked::KIND_QUESTION, payload) unless interface == :non_interactive
|
|
1583
1588
|
@question_desk.request(payload)
|
|
1584
1589
|
end
|
|
1585
1590
|
|
|
1591
|
+
# The :question_asked hooks for a question of the model's about to
|
|
1592
|
+
# open: the fields with the asking step's refs (QuestionAsked.fire).
|
|
1593
|
+
# @return [Hash]
|
|
1594
|
+
def question_asked(kind, fields)
|
|
1595
|
+
QuestionAsked.fire(hooks: @hooks, conversation: live_conversation, kind: kind, fields: fields,
|
|
1596
|
+
cwd: @session&.working_directory, synchronize: ->(&block) { synchronize_events(&block) },
|
|
1597
|
+
visible: ->(content) { question_step_text(content) })
|
|
1598
|
+
end
|
|
1599
|
+
private :question_asked
|
|
1600
|
+
|
|
1601
|
+
# A step's text as the asking slot shows it: the native loop's raw
|
|
1602
|
+
# response without its thoughts and tool-call bodies (a chat step has
|
|
1603
|
+
# neither), then the markup's tokens (OutputFormatter.strip_markup).
|
|
1604
|
+
def question_step_text(content)
|
|
1605
|
+
parser = @kernel.parser
|
|
1606
|
+
OutputFormatter.strip_markup(parser.strip_tool_calls(parser.strip_thought(content)))
|
|
1607
|
+
end
|
|
1608
|
+
private :question_step_text
|
|
1609
|
+
|
|
1586
1610
|
# Open a question for the UIs and wait for its answer
|
|
1587
1611
|
# (QuestionDesk#open_question: BLOCKS until answered or cancelled).
|
|
1588
1612
|
# @param fields [Hash] question:, options:, header:, multi_select:, allow_freeform:, …
|