samagotchi 0.91.0 → 0.93.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 +88 -1
- data/README.md +5 -2
- data/bin/chi +8 -8
- data/docs/architecture.md +20 -16
- data/docs/bundle-authoring.md +2 -1
- data/docs/cli.md +5 -5
- data/docs/configuration.md +3 -1
- data/docs/context.md +5 -3
- data/docs/guardrails.md +4 -5
- data/docs/hooks.md +3 -3
- data/docs/internals/background-tasks.md +2 -2
- data/docs/internals/bundles.md +4 -4
- data/docs/internals/engine.md +27 -8
- data/docs/internals/tool-guardrails.md +10 -2
- data/docs/memory.md +6 -0
- data/docs/plugins.md +30 -13
- data/docs/releasing.md +3 -0
- data/docs/sessions.md +4 -4
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bootstrap/probe.rb +2 -2
- data/lib/samagotchi/bridge.rb +8 -9
- data/lib/samagotchi/bundle_command/check.rb +43 -12
- data/lib/samagotchi/bundle_command/scaffold.rb +12 -0
- data/lib/samagotchi/bundle_command/test_run.rb +12 -0
- data/lib/samagotchi/bundle_command/try/command.rb +164 -0
- data/lib/samagotchi/bundle_command/try/render.rb +80 -0
- data/lib/samagotchi/bundle_command/try/run.rb +35 -17
- data/lib/samagotchi/bundle_command/try/sandbox.rb +113 -34
- data/lib/samagotchi/bundle_command/try/trace.rb +20 -26
- data/lib/samagotchi/bundle_command.rb +11 -213
- data/lib/samagotchi/bundle_session_settings.rb +82 -45
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +6 -5
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +57 -12
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_env.rb +50 -0
- data/lib/samagotchi/child_reports.rb +12 -14
- data/lib/samagotchi/client_id.rb +18 -0
- data/lib/samagotchi/config/restart_watch.rb +112 -0
- data/lib/samagotchi/config.rb +18 -59
- data/lib/samagotchi/engine.rb +26 -26
- data/lib/samagotchi/guardrails/data_readers.rb +34 -30
- data/lib/samagotchi/guardrails/parent_levels.rb +31 -0
- data/lib/samagotchi/guardrails/parent_policy.rb +9 -20
- data/lib/samagotchi/guardrails/shell_lex.rb +28 -7
- data/lib/samagotchi/hook_question.rb +21 -0
- data/lib/samagotchi/hook_routes.rb +6 -6
- data/lib/samagotchi/hooks/registry.rb +7 -5
- data/lib/samagotchi/host_registry.rb +1 -1
- data/lib/samagotchi/kernel_loop.rb +14 -3
- data/lib/samagotchi/llm/anthropic_messages.rb +5 -0
- data/lib/samagotchi/llm/chat_loop.rb +3 -1
- data/lib/samagotchi/llm/errors.rb +4 -4
- data/lib/samagotchi/memory_bundle/builder.rb +3 -2
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +7 -2
- data/lib/samagotchi/memory_bundle/installer.rb +1 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +42 -8
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
- data/lib/samagotchi/parent_answers_command.rb +6 -6
- data/lib/samagotchi/parent_report.rb +12 -0
- data/lib/samagotchi/plugin/context.rb +6 -5
- data/lib/samagotchi/plugin/http_transport.rb +14 -3
- data/lib/samagotchi/plugin/sessions.rb +2 -3
- data/lib/samagotchi/question_desk.rb +12 -9
- data/lib/samagotchi/reply_wait/poll.rb +267 -0
- data/lib/samagotchi/reply_wait.rb +51 -190
- data/lib/samagotchi/sandbox_layout.rb +66 -0
- data/lib/samagotchi/send_command/parent_gate.rb +83 -0
- data/lib/samagotchi/send_command/wait_target.rb +55 -0
- data/lib/samagotchi/send_command.rb +34 -103
- data/lib/samagotchi/session.rb +10 -9
- data/lib/samagotchi/session_inbox.rb +31 -8
- data/lib/samagotchi/session_manager.rb +60 -35
- data/lib/samagotchi/session_metrics.rb +22 -3
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/task_watch.rb +64 -21
- data/lib/samagotchi/terminal_ui/attached_loop.rb +3 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +8 -5
- data/lib/samagotchi/testing/bundle.rb +33 -20
- data/lib/samagotchi/testing/host.rb +2 -8
- data/lib/samagotchi/testing/install.rb +26 -17
- data/lib/samagotchi/testing/recorder.rb +6 -9
- data/lib/samagotchi/testing/sandbox.rb +45 -37
- data/lib/samagotchi/testing/waiting.rb +5 -7
- data/lib/samagotchi/tool_response.rb +28 -6
- data/lib/samagotchi/tools/delegate.rb +10 -10
- data/lib/samagotchi/tools/delegate_cursor.rb +32 -8
- data/lib/samagotchi/tools/delegate_relay.rb +31 -16
- data/lib/samagotchi/tools/delegate_wait.rb +4 -4
- data/lib/samagotchi/tools/task_runtime.rb +2 -2
- data/lib/samagotchi/turn_checkpoint.rb +23 -10
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +46 -63
- data/lib/samagotchi/web/public/app.js +130 -554
- data/lib/samagotchi/web/public/cards_place.js +4 -1
- data/lib/samagotchi/web/public/images.js +6 -0
- data/lib/samagotchi/web/public/index.html +1 -1
- data/lib/samagotchi/web/public/model_pick.js +10 -0
- data/lib/samagotchi/web/public/send_turn.js +173 -0
- data/lib/samagotchi/web/public/session_actions.js +203 -0
- data/lib/samagotchi/web/public/start_page.js +236 -0
- data/lib/samagotchi/web/public/toasts.js +17 -9
- data/lib/samagotchi/web/public/unqueue.js +20 -4
- data/lib/samagotchi/worker.rb +77 -185
- data/lib/samagotchi/worker_idle_exit.rb +20 -5
- data/lib/samagotchi/worker_inbound.rb +5 -5
- data/lib/samagotchi/worker_task_wakes.rb +160 -0
- data/lib/samagotchi/worker_wakes.rb +0 -3
- metadata +15 -1
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "../../analytics_file"
|
|
5
|
+
require_relative "../../bridge/card_store"
|
|
6
|
+
require_relative "../../session"
|
|
5
7
|
require_relative "../../session_metrics"
|
|
8
|
+
require_relative "../../tool_response"
|
|
6
9
|
|
|
7
10
|
module Samagotchi
|
|
8
11
|
class BundleCommand
|
|
@@ -15,25 +18,23 @@ module Samagotchi
|
|
|
15
18
|
|
|
16
19
|
class TraceReader
|
|
17
20
|
DENIAL_TEXT = "No one to approve it (non-interactive run)."
|
|
18
|
-
SESSION_ID = /\A[A-Za-z0-9_-]+\z/
|
|
19
21
|
STDERR_LIMIT = 4000
|
|
20
22
|
|
|
21
23
|
def initialize(state_home:, session_id:)
|
|
22
24
|
@state_home = state_home
|
|
23
|
-
@session_id = session_id if
|
|
25
|
+
@session_id = session_id if Session.valid_id?(session_id)
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def read(exit_code:, answer:, stderr:, sandbox:, timed_out: false)
|
|
27
29
|
return empty_trace(exit_code, answer, stderr, sandbox, timed_out) unless @session_id
|
|
28
30
|
|
|
29
|
-
sessions_dir = File.join(@state_home,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
session = json_file(session_path) || {}
|
|
31
|
+
sessions_dir = File.join(@state_home, Session::STATE_SUBDIR)
|
|
32
|
+
session_dir = Session.session_dir(@session_id, state_dir: sessions_dir)
|
|
33
|
+
session = json_file(Session.session_file(@session_id, state_dir: sessions_dir)) || {}
|
|
33
34
|
messages = Array(session["messages"]).select { |message| message.is_a?(Hash) }
|
|
34
35
|
analytics = AnalyticsFile.read(session_dir) || {}
|
|
35
36
|
entries = card_entries(session_dir)
|
|
36
|
-
tool_rows, denied = tools(messages, analytics
|
|
37
|
+
tool_rows, denied = tools(messages, analytics)
|
|
37
38
|
summary = SessionMetrics.saved_summary(session_dir)
|
|
38
39
|
cost, estimate = cost_summary(summary)
|
|
39
40
|
questions = entries.select { |entry| entry["type"] == "question" }
|
|
@@ -62,7 +63,11 @@ module Samagotchi
|
|
|
62
63
|
timed_out: timed_out)
|
|
63
64
|
end
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
# One row per saved call. A turn a --timeout interrupt cut (a
|
|
67
|
+
# Ctrl-C) saves its calls too, the one it cut short answered as
|
|
68
|
+
# interrupted (ToolResponse.interrupted): a record has no row of its
|
|
69
|
+
# own, so no call shows twice.
|
|
70
|
+
def tools(messages, analytics)
|
|
66
71
|
records = Array(analytics["tool_records"]).select { |record| record.is_a?(Hash) }
|
|
67
72
|
.to_h do |record|
|
|
68
73
|
[[record["iteration"].to_i, record["call_index"].to_i], record]
|
|
@@ -86,6 +91,7 @@ module Samagotchi
|
|
|
86
91
|
is_denied = response && response["content"].to_s.include?(DENIAL_TEXT)
|
|
87
92
|
denied << index if is_denied
|
|
88
93
|
status = is_denied ? "denied" : record&.dig("status").to_s
|
|
94
|
+
status = "interrupted" if status.empty? && response && ToolResponse.interrupted_text?(response["content"])
|
|
89
95
|
status = "unknown" if status.empty?
|
|
90
96
|
{
|
|
91
97
|
index: index,
|
|
@@ -96,24 +102,9 @@ module Samagotchi
|
|
|
96
102
|
}
|
|
97
103
|
end
|
|
98
104
|
end
|
|
99
|
-
rows += unsaved_rows(records, iteration, index) if timed_out
|
|
100
105
|
[rows, denied]
|
|
101
106
|
end
|
|
102
107
|
|
|
103
|
-
# A turn cut by a --timeout interrupt saves only the messages before
|
|
104
|
-
# it (a Ctrl-C), while its tool records stay: the records past the
|
|
105
|
-
# last saved model message are its calls, without their arguments.
|
|
106
|
-
# Only then: a normal run's loop also counts iterations it saves no
|
|
107
|
-
# model message for (a cut's retry, an empty-answer nudge).
|
|
108
|
-
def unsaved_rows(records, saved_iterations, index)
|
|
109
|
-
records.select { |(iteration, _), _| iteration > saved_iterations }.sort.map do |_, record|
|
|
110
|
-
index += 1
|
|
111
|
-
status = record["status"].to_s
|
|
112
|
-
{ index: index, name: record["called_as"] || record["tool"] || "tool", args: "",
|
|
113
|
-
status: status.empty? ? "unknown" : status, duration_ms: record["duration_ms"].to_i }
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
108
|
def call_name(call)
|
|
118
109
|
function = call["function"].is_a?(Hash) ? call["function"] : {}
|
|
119
110
|
call["name"] || function["name"] || "tool"
|
|
@@ -132,10 +123,13 @@ module Samagotchi
|
|
|
132
123
|
args.to_s.gsub(/\s+/, " ").strip[0, 200]
|
|
133
124
|
end
|
|
134
125
|
|
|
126
|
+
# The listing's placement fields: nothing to place in a one-turn run.
|
|
127
|
+
LISTING_FIELDS = %w[turns_since current earlier].freeze
|
|
128
|
+
|
|
129
|
+
# The saved cards as the web reads them (CardStore.saved), with
|
|
130
|
+
# string keys and values, as the JSON trace prints them.
|
|
135
131
|
def card_entries(session_dir)
|
|
136
|
-
|
|
137
|
-
entries = data.is_a?(Hash) ? data["entries"] : []
|
|
138
|
-
Array(entries).select { |entry| entry.is_a?(Hash) }
|
|
132
|
+
JSON.parse(JSON.generate(Bridge::CardStore.saved(session_dir))).map { |entry| entry.except(*LISTING_FIELDS) }
|
|
139
133
|
end
|
|
140
134
|
|
|
141
135
|
def unanswerable?(question)
|
|
@@ -90,46 +90,6 @@ module Samagotchi
|
|
|
90
90
|
chi bundle build --scope system --out ./my-bundle/ identity.md work.md
|
|
91
91
|
TEXT
|
|
92
92
|
TRASH_HELP = "Usage: chi bundle trash [--empty] [--dry-run] [--older-than DAYS]"
|
|
93
|
-
NEW_HELP = <<~TEXT
|
|
94
|
-
Usage: chi bundle new NAME [--dir DIR]
|
|
95
|
-
|
|
96
|
-
Start a bundle in DIR/NAME (default ./NAME; refused when it exists): manifest.yml (shas filled,
|
|
97
|
-
requires_chi this chi), plugin.rb (a /NAME command and an after_tool_call hook), test/NAME_test.rb
|
|
98
|
-
(passes as written) and README.md. Then: chi bundle test, chi bundle check, chi bundle install.
|
|
99
|
-
TEXT
|
|
100
|
-
TEST_HELP = <<~TEXT
|
|
101
|
-
Usage: chi bundle test [PATH] [-- ARGS]
|
|
102
|
-
|
|
103
|
-
Run the tests of the bundle at PATH (default: .) on the Ruby and the lib chi runs on, outside any Bundler env:
|
|
104
|
-
test/**/*_test.rb on Minitest (require "samagotchi/testing/minitest"), or else spec/ on RSpec with the kit
|
|
105
|
-
(-r samagotchi/testing/rspec). ARGS go to the test runner (-- -n /name/, -- -e "a name").
|
|
106
|
-
The tests run the bundle's code as written, with your HOME (the kit's sandbox is per test): run code you'd install.
|
|
107
|
-
Exit status: the tests'; 1 no tests; 2 usage, or minitest / rspec doesn't load.
|
|
108
|
-
TEXT
|
|
109
|
-
CHECK_HELP = <<~TEXT
|
|
110
|
-
Usage: chi bundle check [PATH] [--fix] [--set KEY=VALUE]
|
|
111
|
-
|
|
112
|
-
Check the bundle at PATH (default: .) as an install would take it, one finding per line:
|
|
113
|
-
error|warn|info CODE file[:line]: message (fix: hint)
|
|
114
|
-
It installs and loads the bundle in a sandbox (temp HOME, config and state), not in yours, but its plugin and
|
|
115
|
-
hooks run in this process as written: run it on code you'd install.
|
|
116
|
-
--fix rewrite stale and placeholder sha256 lines in manifest.yml
|
|
117
|
-
--set KEY=VALUE a setting for the load (config.yml bundles: <name>: KEY), repeatable
|
|
118
|
-
Exit status: 0 no errors, 1 errors, 2 usage.
|
|
119
|
-
TEXT
|
|
120
|
-
TRY_HELP = <<~TEXT
|
|
121
|
-
Usage: chi bundle try PATH -p TEXT [--model NAME] [--memory NAME] [--mute NAME] [--toolkit NAME]
|
|
122
|
-
[--thinking LEVEL] [--set KEY=VALUE] [--json] [--keep] [--timeout SECONDS]
|
|
123
|
-
|
|
124
|
-
Install PATH and run one non-interactive turn in an isolated temporary config, state and work directory.
|
|
125
|
-
The user's hosts, keys and ordinary runtime settings are retained; their bundles, hooks, guardrails,
|
|
126
|
-
memories, prompt additions, sessions and attached context sources are not. Shipped guardrails and a
|
|
127
|
-
$1 cost cap are installed for the run. --set KEY=VALUE sets the tried bundle's config.yml bundles entry;
|
|
128
|
-
--set cost-cap.cap=N overrides the cap (unlike chi --set BUNDLE.KEY=VALUE, these are config settings).
|
|
129
|
-
--memory and --mute accept comma-separated names and repeat. --keep retains the sandbox and debug log.
|
|
130
|
-
--json prints the experimental machine-readable trace.
|
|
131
|
-
Exit status: child turn's status; 1 on install failure or timeout; 2 for usage; 130 on Ctrl-C.
|
|
132
|
-
TEXT
|
|
133
93
|
|
|
134
94
|
# The subcommands' flags: --help only; a word not starting with -- is
|
|
135
95
|
# a positional (install's source, -h included). --scope takes whatever
|
|
@@ -153,27 +113,6 @@ module Samagotchi
|
|
|
153
113
|
BUILD_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A--/, dash_values: false) do |f|
|
|
154
114
|
%w[--scope --name --version --description --out].each { |name| f.value name }
|
|
155
115
|
end
|
|
156
|
-
NEW_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A--/) do |f|
|
|
157
|
-
f.value "--dir"
|
|
158
|
-
end
|
|
159
|
-
TEST_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A-/)
|
|
160
|
-
CHECK_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A--/) do |f|
|
|
161
|
-
f.switch "--fix"
|
|
162
|
-
f.value "--set", key: :set, repeat: true
|
|
163
|
-
end
|
|
164
|
-
TRY_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A-/) do |f|
|
|
165
|
-
f.value "-p", "--prompt", key: :prompt
|
|
166
|
-
f.value "--model"
|
|
167
|
-
f.value "--memory", repeat: true
|
|
168
|
-
f.value "--mute", repeat: true
|
|
169
|
-
f.value "--toolkit"
|
|
170
|
-
f.value "--thinking"
|
|
171
|
-
f.value "--set", repeat: true
|
|
172
|
-
f.switch "--json"
|
|
173
|
-
f.switch "--keep"
|
|
174
|
-
f.value "--timeout"
|
|
175
|
-
f.refuse "--attach", "--shared", "chi bundle try can't use --attach or --shared"
|
|
176
|
-
end
|
|
177
116
|
TRASH_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A--/) do |f|
|
|
178
117
|
f.switch "--empty"
|
|
179
118
|
f.switch "--dry-run"
|
|
@@ -855,14 +794,14 @@ module Samagotchi
|
|
|
855
794
|
end
|
|
856
795
|
|
|
857
796
|
def new_bundle(rest)
|
|
858
|
-
|
|
797
|
+
require_relative "bundle_command/scaffold"
|
|
798
|
+
parsed = parse_flags(Scaffold::FLAGS, rest, "new", help: Scaffold::HELP)
|
|
859
799
|
return parsed if parsed.is_a?(Integer)
|
|
860
800
|
|
|
861
801
|
unless parsed.args.size == 1
|
|
862
|
-
@stderr.puts
|
|
802
|
+
@stderr.puts Scaffold::HELP.lines.first
|
|
863
803
|
return CLI::Exit::USAGE
|
|
864
804
|
end
|
|
865
|
-
require_relative "bundle_command/scaffold"
|
|
866
805
|
scaffold = Scaffold.new(parsed.args.first, parent: parsed.options.fetch(:dir, "."))
|
|
867
806
|
begin
|
|
868
807
|
written = scaffold.write
|
|
@@ -885,20 +824,21 @@ module Samagotchi
|
|
|
885
824
|
def test_bundle(rest)
|
|
886
825
|
split = rest.index("--")
|
|
887
826
|
own, args = split ? [rest[0...split], rest[(split + 1)..]] : [rest, []]
|
|
888
|
-
|
|
827
|
+
require_relative "bundle_command/test_run"
|
|
828
|
+
parsed = parse_flags(TestRun::FLAGS, own, "test", help: TestRun::HELP)
|
|
889
829
|
return parsed if parsed.is_a?(Integer)
|
|
890
830
|
|
|
891
831
|
dir = parsed.args.first || "."
|
|
892
832
|
unless parsed.args.size <= 1 && File.directory?(dir)
|
|
893
|
-
@stderr.puts parsed.args.size > 1 ?
|
|
833
|
+
@stderr.puts parsed.args.size > 1 ? TestRun::HELP.lines.first : "chi bundle test: #{dir} isn't a folder"
|
|
894
834
|
return CLI::Exit::USAGE
|
|
895
835
|
end
|
|
896
|
-
require_relative "bundle_command/test_run"
|
|
897
836
|
TestRun.new(dir, args: args, stdout: @stdout, stderr: @stderr).run
|
|
898
837
|
end
|
|
899
838
|
|
|
900
839
|
def check(rest)
|
|
901
|
-
|
|
840
|
+
require_relative "bundle_command/check"
|
|
841
|
+
parsed = parse_flags(Check::FLAGS, rest, "check", help: Check::HELP)
|
|
902
842
|
return parsed if parsed.is_a?(Integer)
|
|
903
843
|
|
|
904
844
|
settings = check_settings(parsed.options.fetch(:set, []), label: "check") or return CLI::Exit::USAGE
|
|
@@ -911,157 +851,15 @@ module Samagotchi
|
|
|
911
851
|
@stderr.puts "chi bundle check: #{dir} isn't a folder"
|
|
912
852
|
return CLI::Exit::USAGE
|
|
913
853
|
end
|
|
914
|
-
require_relative "bundle_command/check"
|
|
915
854
|
Check.new(dir, fix: parsed.options.fetch(:fix, false), settings: settings, stdout: @stdout).run
|
|
916
855
|
end
|
|
917
856
|
|
|
918
857
|
def try_bundle(rest)
|
|
919
|
-
|
|
858
|
+
require_relative "bundle_command/try/command"
|
|
859
|
+
parsed = parse_flags(Try::Command::FLAGS, rest, "try", help: Try::Command::HELP)
|
|
920
860
|
return parsed if parsed.is_a?(Integer)
|
|
921
861
|
|
|
922
|
-
|
|
923
|
-
if options[:prompt].nil? || parsed.args.size != 1
|
|
924
|
-
@stderr.puts "Usage: chi bundle try PATH -p TEXT [options]"
|
|
925
|
-
return CLI::Exit::USAGE
|
|
926
|
-
end
|
|
927
|
-
path = parsed.args.first
|
|
928
|
-
unless File.directory?(path)
|
|
929
|
-
@stderr.puts "chi bundle try: #{path} isn't a folder"
|
|
930
|
-
return CLI::Exit::USAGE
|
|
931
|
-
end
|
|
932
|
-
if options.values_at(:model, :toolkit, :thinking, :memory, :mute).flatten.compact.any?(&:empty?)
|
|
933
|
-
@stderr.puts "chi bundle try: model, toolkit, thinking, memory and mute values cannot be empty"
|
|
934
|
-
return CLI::Exit::USAGE
|
|
935
|
-
end
|
|
936
|
-
require_relative "thinking"
|
|
937
|
-
if options[:thinking] && !Thinking::LEVELS.map(&:to_s).include?(options[:thinking])
|
|
938
|
-
@stderr.puts "chi bundle try: --thinking takes #{Thinking::LEVELS.join(", ")}, not #{options[:thinking]}"
|
|
939
|
-
return CLI::Exit::USAGE
|
|
940
|
-
end
|
|
941
|
-
|
|
942
|
-
timeout = parse_try_timeout(options[:timeout])
|
|
943
|
-
return CLI::Exit::USAGE unless timeout
|
|
944
|
-
|
|
945
|
-
set_pairs = []
|
|
946
|
-
cost_cap = 1
|
|
947
|
-
cost_cap_override = false
|
|
948
|
-
require_relative "bundle_session_settings"
|
|
949
|
-
Array(options[:set]).each do |pair|
|
|
950
|
-
key, sep, value = pair.partition("=")
|
|
951
|
-
if sep.empty?
|
|
952
|
-
@stderr.puts "chi bundle try: --set takes KEY=VALUE, not #{pair}"
|
|
953
|
-
return CLI::Exit::USAGE
|
|
954
|
-
end
|
|
955
|
-
if key == "cost-cap.cap"
|
|
956
|
-
cost_cap = BundleSessionSettings.typed(value)
|
|
957
|
-
cost_cap_override = true
|
|
958
|
-
unless (cost_cap.is_a?(Numeric) && cost_cap >= 0) || cost_cap == false
|
|
959
|
-
@stderr.puts "chi bundle try: --set cost-cap.cap takes a non-negative number or off"
|
|
960
|
-
return CLI::Exit::USAGE
|
|
961
|
-
end
|
|
962
|
-
else
|
|
963
|
-
set_pairs << pair
|
|
964
|
-
end
|
|
965
|
-
end
|
|
966
|
-
settings = check_settings(set_pairs, label: "try")
|
|
967
|
-
return CLI::Exit::USAGE unless settings
|
|
968
|
-
|
|
969
|
-
require_relative "bundle_command/try/sandbox"
|
|
970
|
-
require_relative "bundle_command/try/run"
|
|
971
|
-
require_relative "bundle_command/try/trace"
|
|
972
|
-
sandbox = nil
|
|
973
|
-
begin
|
|
974
|
-
sandbox = Try::Sandbox.new(path, bundle_settings: settings, cost_cap: cost_cap,
|
|
975
|
-
cost_cap_override: cost_cap_override)
|
|
976
|
-
sandbox.install
|
|
977
|
-
result = Try::Run.new(sandbox).run(prompt: options[:prompt], options: options, timeout: timeout)
|
|
978
|
-
trace = Try::TraceReader.new(state_home: sandbox.state_home, session_id: result.session_id)
|
|
979
|
-
.read(exit_code: result.exit_status, answer: result.stdout, stderr: result.stderr,
|
|
980
|
-
sandbox: options[:keep] ? sandbox.root : nil, timed_out: result.timed_out)
|
|
981
|
-
render_try_trace(trace, json: options[:json], child_stderr: result.stderr)
|
|
982
|
-
@stderr.puts "chi bundle try: timed out after #{timeout} seconds" if result.timed_out
|
|
983
|
-
result.exit_status
|
|
984
|
-
rescue Try::Sandbox::Error => e
|
|
985
|
-
@stderr.puts e.message
|
|
986
|
-
e.status
|
|
987
|
-
ensure
|
|
988
|
-
report_try_sandbox(sandbox) if options[:keep] && sandbox
|
|
989
|
-
sandbox&.cleanup unless options[:keep]
|
|
990
|
-
end
|
|
991
|
-
end
|
|
992
|
-
|
|
993
|
-
def render_try_trace(trace, json:, child_stderr:)
|
|
994
|
-
error_lines = child_stderr.lines.reject { |line| line.start_with?("Session: ") }
|
|
995
|
-
@stderr.write(error_lines.join)
|
|
996
|
-
if json
|
|
997
|
-
render_trace_lines(trace, @stderr, include_answer: false)
|
|
998
|
-
@stdout.puts JSON.generate(trace.to_h.transform_keys(&:to_s))
|
|
999
|
-
else
|
|
1000
|
-
render_trace_lines(trace, @stdout, include_answer: true)
|
|
1001
|
-
end
|
|
1002
|
-
end
|
|
1003
|
-
|
|
1004
|
-
def report_try_sandbox(sandbox)
|
|
1005
|
-
@stderr.puts "sandbox: #{sandbox.root}"
|
|
1006
|
-
@stderr.puts "debug log: #{File.join(sandbox.state_home, "samagotchi", "samagotchi.log")}"
|
|
1007
|
-
end
|
|
1008
|
-
|
|
1009
|
-
def render_trace_lines(trace, output, include_answer:)
|
|
1010
|
-
output.puts "tools:"
|
|
1011
|
-
trace.tools.each do |tool|
|
|
1012
|
-
output.puts format(" %d %-16s %-7s %dms %s", tool[:index], tool[:name], tool[:status],
|
|
1013
|
-
tool[:duration_ms], JSON.generate(tool[:args].to_s[0, 60]))
|
|
1014
|
-
end
|
|
1015
|
-
trace.notices.each { |notice| output.puts "notices: [#{notice["hook"] || "hook"}] #{notice["text"]}" }
|
|
1016
|
-
trace.cards.each do |card|
|
|
1017
|
-
output.puts "card: #{[card["title"], card["body"]].compact.join(" — ")}"
|
|
1018
|
-
end
|
|
1019
|
-
trace.questions.each do |entry|
|
|
1020
|
-
question = entry.dig("pending_question", "question").to_s
|
|
1021
|
-
outcome = if entry["cancelled"] && entry["reason"] == "non_interactive"
|
|
1022
|
-
"unanswerable (non_interactive)"
|
|
1023
|
-
elsif entry["cancelled"]
|
|
1024
|
-
"cancelled (#{entry["reason"]})"
|
|
1025
|
-
elsif entry["answer"]
|
|
1026
|
-
"answered"
|
|
1027
|
-
else
|
|
1028
|
-
"unanswered"
|
|
1029
|
-
end
|
|
1030
|
-
output.puts "questions: #{JSON.generate(question)} -> #{outcome}"
|
|
1031
|
-
end
|
|
1032
|
-
if trace.interactive_blocked
|
|
1033
|
-
count = trace.denied.size + trace.questions.count { |question| question["cancelled"] && question["reason"] == "non_interactive" }
|
|
1034
|
-
output.puts "warn: #{count} question(s)/call(s) couldn't be answered"
|
|
1035
|
-
end
|
|
1036
|
-
if include_answer
|
|
1037
|
-
answer_lines = trace.answer.lines
|
|
1038
|
-
output.puts "answer:"
|
|
1039
|
-
answer_lines.first(10).each { |line| output.puts " #{line.chomp}" }
|
|
1040
|
-
output.puts " … (truncated)" if answer_lines.size > 10
|
|
1041
|
-
end
|
|
1042
|
-
tokens = trace.tokens
|
|
1043
|
-
prompt = tokens[:prompt] || "unknown"
|
|
1044
|
-
completion = tokens[:completion] || "unknown"
|
|
1045
|
-
cost = if trace.cost
|
|
1046
|
-
"#{"~" if trace.cost_is_estimate}$#{format("%.4f", trace.cost)}"
|
|
1047
|
-
else
|
|
1048
|
-
"unknown"
|
|
1049
|
-
end
|
|
1050
|
-
output.puts "tokens: #{prompt} in / #{completion} out cost #{cost} exit #{trace.exit_code}" \
|
|
1051
|
-
"#{" (#{trace.session_id})" if trace.session_id}"
|
|
1052
|
-
end
|
|
1053
|
-
|
|
1054
|
-
def parse_try_timeout(value)
|
|
1055
|
-
return 300 unless value
|
|
1056
|
-
|
|
1057
|
-
timeout = Float(value)
|
|
1058
|
-
return timeout if timeout.positive? && timeout.finite?
|
|
1059
|
-
|
|
1060
|
-
@stderr.puts "chi bundle try: --timeout must be a positive number"
|
|
1061
|
-
nil
|
|
1062
|
-
rescue ArgumentError
|
|
1063
|
-
@stderr.puts "chi bundle try: --timeout must be a positive number"
|
|
1064
|
-
nil
|
|
862
|
+
Try::Command.new(stdout: @stdout, stderr: @stderr).run(parsed)
|
|
1065
863
|
end
|
|
1066
864
|
|
|
1067
865
|
# --set KEY=VALUE values as typed (BundleSessionSettings.typed), or nil
|
|
@@ -9,9 +9,10 @@ module Samagotchi
|
|
|
9
9
|
# value}}, string keys, scalar values (.typed). A bundle offers the keys a
|
|
10
10
|
# session may set in its manifest's session_settings: (recorded at install,
|
|
11
11
|
# InstalledBundle#session_settings); any other bundle or key is refused, so
|
|
12
|
-
# a typo isn't saved
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# a typo isn't saved, and so is a value of another type than the manifest
|
|
13
|
+
# gives the key (session_settings: {cap: number}). The session keeps them
|
|
14
|
+
# (Session#bundle_settings, SessionSetup) and Plugin::Context#settings
|
|
15
|
+
# lays them over config.yml's bundles: <name>:.
|
|
15
16
|
module BundleSessionSettings
|
|
16
17
|
# A refused --set (or request field): the message says why.
|
|
17
18
|
class Error < ArgumentError; end
|
|
@@ -22,25 +23,44 @@ module Samagotchi
|
|
|
22
23
|
# The values a setting may take.
|
|
23
24
|
SCALARS = [String, Integer, Float, TrueClass, FalseClass].freeze
|
|
24
25
|
REQUEST_SHAPE = "bundle_settings is an object of bundle => {key => scalar}"
|
|
25
|
-
# What .typed reads as a number: no leading zero, no
|
|
26
|
+
# What .typed reads as a number: no leading zero, no _; a decimal may
|
|
27
|
+
# leave out either side of its point (.5, 5.) and take an exponent
|
|
28
|
+
# (1e1, 2.5e-3), which YAML 1.1 leaves as text.
|
|
26
29
|
INTEGER = /\A-?(?:0|[1-9]\d*)\z/
|
|
27
|
-
DECIMAL = /\A-?(?:0|[1-9]\d*)
|
|
30
|
+
DECIMAL = /\A-?(?:(?:0|[1-9]\d*)(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?\z/
|
|
31
|
+
# YAML 1.1's booleans (config.yml's), any case.
|
|
32
|
+
TRUE_WORDS = %w[true yes on].freeze
|
|
33
|
+
FALSE_WORDS = %w[false no off].freeze
|
|
28
34
|
# The shipped bundles' sources (a stale record's hint reads them).
|
|
29
35
|
SHIPPED_DIR = File.expand_path("bundles", __dir__)
|
|
30
36
|
|
|
31
37
|
# The installed bundles a session may set settings of: +offered+ the
|
|
32
38
|
# keys of each bundle whose plugin loads ({name => keys}), +notes+ what
|
|
33
39
|
# a refusal adds for a bundle ({name => text}): why its plugin doesn't
|
|
34
|
-
# load, or how to refresh a record from before session_settings
|
|
35
|
-
|
|
40
|
+
# load, or how to refresh a record from before session_settings;
|
|
41
|
+
# +types+ the types its manifest gives keys ({name => {key => type}}).
|
|
42
|
+
Availability = Data.define(:offered, :notes, :types) do
|
|
43
|
+
def initialize(offered:, notes:, types: {}) = super
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# What each type (MemoryBundle::Manifest::SESSION_SETTING_TYPES) takes,
|
|
47
|
+
# as a value .typed gives it, and how a refusal names it. A number may
|
|
48
|
+
# be off (false), as a bundle's own settings are turned off.
|
|
49
|
+
TYPES = {
|
|
50
|
+
"number" => [->(value) { (value.is_a?(Numeric) && value.finite?) || value == false }, "a number (or off)"],
|
|
51
|
+
"boolean" => [->(value) { [true, false].include?(value) }, "true or false"],
|
|
52
|
+
"string" => [->(value) { value.is_a?(String) }, "text"]
|
|
53
|
+
}.freeze
|
|
36
54
|
|
|
37
55
|
module_function
|
|
38
56
|
|
|
39
57
|
# --set's values as typed, in order: a later one of the same key wins.
|
|
58
|
+
# A key +types+ gives as string keeps its text as given (.value_for).
|
|
40
59
|
# @param args [Array<String>] "BUNDLE.KEY=VALUE" each
|
|
60
|
+
# @param types [Hash{String => Hash{String => String}}] Availability#types
|
|
41
61
|
# @return [Hash{String => Hash{String => Object}}] frozen
|
|
42
62
|
# @raise [Error] a value of another shape
|
|
43
|
-
def parse(args)
|
|
63
|
+
def parse(args, types: {})
|
|
44
64
|
settings = {}
|
|
45
65
|
Array(args).each do |arg|
|
|
46
66
|
target, sep, raw = arg.to_s.partition("=")
|
|
@@ -51,7 +71,7 @@ module Samagotchi
|
|
|
51
71
|
raise Error, "--set #{arg}: one key under the bundle (BUNDLE.KEY), no nested path" if keys.size > 1
|
|
52
72
|
raise Error, "--set #{arg}: give a value" if raw.strip.empty?
|
|
53
73
|
|
|
54
|
-
(settings[bundle] ||= {})[keys.first] =
|
|
74
|
+
(settings[bundle] ||= {})[keys.first] = value_for(raw, types.dig(bundle, keys.first))
|
|
55
75
|
end
|
|
56
76
|
deep_freeze(settings)
|
|
57
77
|
end
|
|
@@ -60,18 +80,23 @@ module Samagotchi
|
|
|
60
80
|
# @param bundles [Hash{String => Array<String>}] the installed bundles'
|
|
61
81
|
# offered keys (.offered)
|
|
62
82
|
# @param notes [Hash{String => String}] Availability#notes
|
|
83
|
+
# @param types [Hash{String => Hash{String => String}}] Availability#types:
|
|
84
|
+
# a typed key's value must be of its type (TYPES)
|
|
63
85
|
# @param flag [String] what a refusal names ("--set", a request field)
|
|
64
86
|
# @return [Hash] +settings+
|
|
65
87
|
# @raise [Error]
|
|
66
|
-
def check!(settings, bundles: offered, notes: {}, flag: "--set")
|
|
88
|
+
def check!(settings, bundles: offered, notes: {}, types: {}, flag: "--set")
|
|
67
89
|
settings.each do |bundle, values|
|
|
68
90
|
keys = bundles[bundle]
|
|
69
91
|
note = notes[bundle]
|
|
70
|
-
values.
|
|
92
|
+
values.each do |key, value|
|
|
71
93
|
where = "#{flag} #{bundle}.#{key}"
|
|
72
94
|
raise Error, "#{where}: #{note || "no installed bundle #{bundle} (#{takers(bundles)})"}" if keys.nil?
|
|
73
95
|
raise Error, "#{where}: #{bundle} takes no session settings#{" #{note}" if note}" if keys.empty?
|
|
74
96
|
raise Error, "#{where}: #{bundle} takes #{keys.join(", ")}" unless keys.include?(key)
|
|
97
|
+
|
|
98
|
+
takes, what = TYPES[types.dig(bundle, key)]
|
|
99
|
+
raise Error, "#{where}: #{bundle} takes #{what} for #{key}, not #{value.inspect}" if takes && !takes.call(value)
|
|
75
100
|
end
|
|
76
101
|
end
|
|
77
102
|
settings
|
|
@@ -80,40 +105,59 @@ module Samagotchi
|
|
|
80
105
|
# --set's values parsed and checked (.parse, .check!) against the
|
|
81
106
|
# installed bundles (.availability).
|
|
82
107
|
def from_flags(args, available: availability)
|
|
83
|
-
check!(parse(args), bundles: available.offered, notes: available.notes)
|
|
108
|
+
check!(parse(args, types: available.types), bundles: available.offered, notes: available.notes, types: available.types)
|
|
84
109
|
end
|
|
85
110
|
|
|
86
111
|
# A value as --set (and a request's string) gives it: an integer
|
|
87
|
-
# ("3", "-1"), a decimal ("2.5"
|
|
88
|
-
#
|
|
89
|
-
#
|
|
112
|
+
# ("3", "-1"), a decimal ("2.5", ".5", "1e1"), a boolean as config.yml's
|
|
113
|
+
# YAML reads one (true/yes/on, false/no/off, any case); any other string
|
|
114
|
+
# stays as given ("010", "0x10", "3:30", "y", "null"). A value that
|
|
115
|
+
# isn't a String (a JSON number or boolean) is kept.
|
|
90
116
|
def typed(value)
|
|
91
117
|
return value unless value.is_a?(String)
|
|
118
|
+
return Integer(value, 10) if INTEGER.match?(value)
|
|
92
119
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
when "false", "off" then false
|
|
98
|
-
else value
|
|
120
|
+
if DECIMAL.match?(value)
|
|
121
|
+
number = Float(float_text(value))
|
|
122
|
+
# 1e400 is Infinity: no cap reaches it and JSON can't save it.
|
|
123
|
+
return number.finite? ? number : value
|
|
99
124
|
end
|
|
125
|
+
|
|
126
|
+
word = value.downcase
|
|
127
|
+
return true if TRUE_WORDS.include?(word)
|
|
128
|
+
return false if FALSE_WORDS.include?(word)
|
|
129
|
+
|
|
130
|
+
value
|
|
100
131
|
end
|
|
101
132
|
|
|
133
|
+
# A value for a key of +type+: a string key's text as given (--set
|
|
134
|
+
# b.name=yes stays "yes"), any other's .typed.
|
|
135
|
+
def value_for(value, type) = type == "string" ? value : typed(value)
|
|
136
|
+
|
|
137
|
+
# A decimal DECIMAL matches as Float reads it on every Ruby: a point
|
|
138
|
+
# with no digit after it ("5.", "5.e1") gets a 0, which Ruby 3.3's
|
|
139
|
+
# Float wants.
|
|
140
|
+
def float_text(value) = value.sub(/\.(?=e|\z)/i, ".0")
|
|
141
|
+
|
|
102
142
|
# POST /api/sessions bundle_settings: nil for none, else an object of
|
|
103
|
-
# objects of scalars (a string typed as --set
|
|
143
|
+
# objects of scalars (a string typed as --set types it), checked like
|
|
104
144
|
# --set; an empty bundle entry is dropped.
|
|
105
145
|
# @param bundles [Hash, nil] the offered keys (.availability's by default)
|
|
146
|
+
# @param types [Hash] their types, with +bundles+ (Availability#types)
|
|
106
147
|
# @return [Hash] frozen
|
|
107
148
|
# @raise [Error]
|
|
108
|
-
def from_request(raw, bundles: nil)
|
|
149
|
+
def from_request(raw, bundles: nil, types: {})
|
|
109
150
|
return {} if raw.nil?
|
|
110
151
|
raise Error, REQUEST_SHAPE unless raw.is_a?(Hash) && raw.all? { |_, values| scalar_map?(values) }
|
|
111
152
|
|
|
112
|
-
|
|
113
|
-
return
|
|
153
|
+
given = normalize(raw)
|
|
154
|
+
return given if given.empty?
|
|
114
155
|
|
|
115
|
-
available = bundles ? Availability.new(offered: bundles, notes: {}) : availability
|
|
116
|
-
|
|
156
|
+
available = bundles ? Availability.new(offered: bundles, notes: {}, types: types) : availability
|
|
157
|
+
settings = normalize(given.to_h do |bundle, values|
|
|
158
|
+
[bundle, values.to_h { |key, value| [key, value_for(value, available.types.dig(bundle, key))] }]
|
|
159
|
+
end)
|
|
160
|
+
check!(settings, bundles: available.offered, notes: available.notes, types: available.types, flag: "bundle_settings")
|
|
117
161
|
end
|
|
118
162
|
|
|
119
163
|
# Each installed bundle's offered keys, by name (Availability#offered).
|
|
@@ -133,6 +177,7 @@ module Samagotchi
|
|
|
133
177
|
dir ||= MemoryBundle::Provenance.bundles_dir
|
|
134
178
|
offered = {}
|
|
135
179
|
notes = {}
|
|
180
|
+
types = {}
|
|
136
181
|
MemoryBundle::Provenance.each_installed(dir: dir) do |name, bundle|
|
|
137
182
|
next if bundle.error?
|
|
138
183
|
|
|
@@ -141,11 +186,12 @@ module Samagotchi
|
|
|
141
186
|
next
|
|
142
187
|
end
|
|
143
188
|
offered[name] = bundle.session_settings
|
|
189
|
+
types[name] = bundle.session_setting_types unless bundle.session_setting_types.empty?
|
|
144
190
|
if bundle.session_settings.empty? && shipped_settings?(shipped_dir, name)
|
|
145
191
|
notes[name] = "(run `chi update` or `chi bundle upgrade #{name}` to refresh it)"
|
|
146
192
|
end
|
|
147
193
|
end
|
|
148
|
-
Availability.new(offered: offered, notes: notes)
|
|
194
|
+
Availability.new(offered: offered, notes: notes, types: types)
|
|
149
195
|
end
|
|
150
196
|
|
|
151
197
|
# A session file's value, read tolerantly: the bundles that map to
|
|
@@ -165,24 +211,14 @@ module Samagotchi
|
|
|
165
211
|
end)
|
|
166
212
|
end
|
|
167
213
|
|
|
168
|
-
# Why +bundle+'s plugin doesn't load
|
|
169
|
-
# when it does or has none.
|
|
214
|
+
# Why +bundle+'s plugin doesn't load, as chi's loader decides it
|
|
215
|
+
# (Plugin::Loader.unloadable_reason), nil when it does or has none.
|
|
170
216
|
def unloadable(dir, name, bundle)
|
|
171
217
|
return nil unless bundle.plugin
|
|
172
218
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
file = File.join(dir, name, "plugin", bundle.plugin.file.to_s)
|
|
177
|
-
return "the file is missing" unless File.file?(file)
|
|
178
|
-
|
|
179
|
-
return nil if MemoryBundle::Provenance.unchecked?(bundle.plugin.sha256)
|
|
180
|
-
|
|
181
|
-
expected = MemoryBundle::Provenance.recorded_sha(bundle.plugin.sha256)
|
|
182
|
-
return "no sha256 recorded for it (reinstall the bundle)" if expected.empty?
|
|
183
|
-
|
|
184
|
-
actual = MemoryBundle::Provenance.file_sha(file)
|
|
185
|
-
"its sha256 differs from the installed one (edited after install? reinstall the bundle)" unless actual == expected
|
|
219
|
+
require_relative "plugin/loader"
|
|
220
|
+
# +dir+, not Provenance#plugin_path's bundles_dir: a spec's records live elsewhere.
|
|
221
|
+
Plugin::Loader.unloadable_reason(File.join(dir, name, "plugin", bundle.plugin.file.to_s), bundle)
|
|
186
222
|
end
|
|
187
223
|
|
|
188
224
|
# Whether chi ships a bundle +name+ whose manifest lists session_settings.
|
|
@@ -191,8 +227,9 @@ module Samagotchi
|
|
|
191
227
|
return false unless File.file?(path)
|
|
192
228
|
|
|
193
229
|
raw = YAML.safe_load_file(path)
|
|
194
|
-
raw.is_a?(Hash)
|
|
195
|
-
|
|
230
|
+
listed = raw.is_a?(Hash) ? raw["session_settings"] : nil
|
|
231
|
+
(listed.is_a?(Array) || listed.is_a?(Hash)) && !listed.empty?
|
|
232
|
+
rescue Psych::Exception, SystemCallError
|
|
196
233
|
false
|
|
197
234
|
end
|
|
198
235
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: core
|
|
3
|
-
version: 0.3.
|
|
3
|
+
version: 0.3.4
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Recommended safety set"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
# Its members' highest floor (cost-cap's)
|
|
8
|
-
requires_chi: ">= 0.
|
|
8
|
+
requires_chi: ">= 0.92.0"
|
|
9
9
|
includes:
|
|
10
10
|
- loop-guard
|
|
11
11
|
- check-in
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cost-cap
|
|
3
|
-
version: 0.2.
|
|
3
|
+
version: 0.2.1
|
|
4
4
|
scope: system
|
|
5
5
|
description: "A per-session $ cap: at the cap the turn pauses on Continue (another cap's worth) / Stop, a warning at 80% goes to the user only; /cap shows or sets it"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
plugin:
|
|
8
8
|
file: plugin.rb
|
|
9
|
-
sha256: sha256:
|
|
9
|
+
sha256: sha256:28d632cd19117ef359acc6aa1c621081c14c018772b7fe1efd6284e346fea135
|
|
10
10
|
# ctx.cost is new in chi 0.83.0; ask_user's effects: in the next chi
|
|
11
|
-
requires_chi: ">= 0.
|
|
12
|
-
#
|
|
13
|
-
|
|
11
|
+
requires_chi: ">= 0.92.0"
|
|
12
|
+
# session_settings: as a mapping of keys to types is new
|
|
13
|
+
# chi --set cost-cap.cap=3: a session's own cap (ctx.settings["cap"]), a number or off
|
|
14
|
+
session_settings: {cap: number}
|