samagotchi 0.141.0 → 0.142.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 +46 -1
- data/README.md +1 -1
- data/bin/chi +2 -0
- data/docs/architecture.md +12 -283
- data/docs/bundle-authoring.md +1 -0
- data/docs/cli.md +15 -7
- data/docs/configuration.md +57 -18
- data/docs/context.md +2 -2
- data/docs/guardrails.md +20 -11
- data/docs/hooks.md +9 -7
- data/docs/internals/attached-context.md +226 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +380 -0
- data/docs/internals/bundles.md +1 -0
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +24 -18
- data/docs/internals/participants.md +13 -12
- data/docs/internals/remote-sessions.md +1 -1
- data/docs/internals/{tool-guardrails.md → tool-output-limits.md} +21 -2
- data/docs/memory.md +31 -1
- data/docs/plugins.md +122 -1
- data/docs/sessions.md +50 -26
- data/docs/system-one.md +122 -0
- data/docs/testing.md +3 -1
- data/lib/samagotchi/bridge.rb +2 -0
- data/lib/samagotchi/bundles/check-in/manifest.yml +6 -3
- data/lib/samagotchi/bundles/check-in/plugin.rb +101 -4
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +74 -14
- data/lib/samagotchi/context_candidate_choice.rb +8 -0
- data/lib/samagotchi/context_candidates_log.rb +25 -1
- data/lib/samagotchi/context_judge.rb +6 -2
- data/lib/samagotchi/engine.rb +68 -7
- data/lib/samagotchi/extension_load.rb +3 -2
- data/lib/samagotchi/guardrail_wiring.rb +36 -31
- data/lib/samagotchi/guardrails/chi_dirs.rb +7 -4
- data/lib/samagotchi/guardrails/path_resolve.rb +1 -1
- data/lib/samagotchi/guardrails/protected_paths.rb +7 -4
- data/lib/samagotchi/guardrails_command.rb +3 -3
- data/lib/samagotchi/judgments_command.rb +155 -0
- data/lib/samagotchi/kernel_loop.rb +13 -1
- data/lib/samagotchi/llm/chat_loop.rb +74 -10
- data/lib/samagotchi/llm/errors.rb +7 -5
- data/lib/samagotchi/llm/stray_tool_call.rb +61 -0
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/memory_bundle/entry_size.rb +100 -0
- data/lib/samagotchi/plugin/api.rb +33 -2
- data/lib/samagotchi/plugin/context.rb +49 -1
- data/lib/samagotchi/plugin/judgments.rb +72 -0
- data/lib/samagotchi/plugin/loader.rb +3 -2
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/self_report.rb +36 -0
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_commands.rb +3 -0
- data/lib/samagotchi/session_manager.rb +13 -2
- data/lib/samagotchi/session_retention.rb +10 -0
- data/lib/samagotchi/side_judgments.rb +12 -1
- data/lib/samagotchi/system_one/answer.rb +63 -11
- data/lib/samagotchi/system_one/approval_label.rb +56 -0
- data/lib/samagotchi/system_one/approval_question.rb +2 -0
- data/lib/samagotchi/system_one/client.rb +43 -17
- data/lib/samagotchi/system_one/fake_backend.rb +1 -1
- data/lib/samagotchi/system_one/http_backend.rb +12 -4
- data/lib/samagotchi/system_one/judgment_settings.rb +61 -0
- data/lib/samagotchi/system_one/judgments.rb +260 -0
- data/lib/samagotchi/system_one/log.rb +4 -2
- data/lib/samagotchi/system_one/recording.rb +117 -0
- data/lib/samagotchi/system_one/runs.rb +179 -0
- data/lib/samagotchi/system_one/runs_store.rb +230 -0
- data/lib/samagotchi/system_one.rb +9 -0
- data/lib/samagotchi/testing/bundle.rb +9 -2
- data/lib/samagotchi/testing/host.rb +2 -1
- data/lib/samagotchi/testing/judgments.rb +185 -0
- data/lib/samagotchi/testing/recorder.rb +5 -0
- data/lib/samagotchi/testing/recording_api.rb +1 -1
- data/lib/samagotchi/tool_output_cut.rb +26 -6
- data/lib/samagotchi/tool_response.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +63 -4
- data/lib/samagotchi/toolkit_suggest.rb +11 -2
- data/lib/samagotchi/tools/memory.rb +28 -0
- data/lib/samagotchi/turn_note.rb +9 -0
- data/lib/samagotchi/version.rb +1 -1
- metadata +16 -2
|
@@ -26,6 +26,13 @@
|
|
|
26
26
|
# when the session comes back (a worker's restart), before its first turn
|
|
27
27
|
# or command. Another session starts from the settings.
|
|
28
28
|
#
|
|
29
|
+
# Each check-in also asks System 1 `check-in.progress` (docs/system-one.md),
|
|
30
|
+
# off until the user sets a mode for it: the turn's prompt, the calls since
|
|
31
|
+
# the last check-in and the count and elapsed time go, on a side thread, to
|
|
32
|
+
# the cheap judgment model. Nothing is done with the answer: it is saved,
|
|
33
|
+
# and what the user then did with the card is saved as a label on that run.
|
|
34
|
+
# The card, the nudge and the notify are the same either way.
|
|
35
|
+
#
|
|
29
36
|
# Settings (config.yml, bundles: check-in:):
|
|
30
37
|
# after: 50 tool calls in one turn before the first check-in
|
|
31
38
|
# every: 50 then again every this many more
|
|
@@ -43,6 +50,15 @@ class Plugin
|
|
|
43
50
|
DEFAULT_IGNORE = %w[task_wait task_get delegate_result].freeze
|
|
44
51
|
LAST_TOOLS = 5
|
|
45
52
|
USAGE = "usage: /checkin [on|off|<calls>|mode ask|nudge|notify|nudge|later|stop]"
|
|
53
|
+
# The audit judgment (docs/plugins.md, Judgments): one noul question per
|
|
54
|
+
# check-in, answered on a side thread and never acted on.
|
|
55
|
+
PROGRESS = "progress"
|
|
56
|
+
PROGRESS_QUESTION = "Is this turn still making progress toward what the user asked?"
|
|
57
|
+
# The calls since the last check-in, each cut as it is captured and kept
|
|
58
|
+
# together under about 4 KB (the oldest go first).
|
|
59
|
+
CALL_CHARS = 120
|
|
60
|
+
RESULT_CHARS = 200
|
|
61
|
+
CALLS_TOTAL = 4096
|
|
46
62
|
|
|
47
63
|
def initialize(settings = {})
|
|
48
64
|
@after = positive(settings["after"]) || 50
|
|
@@ -59,7 +75,14 @@ class Plugin
|
|
|
59
75
|
end
|
|
60
76
|
|
|
61
77
|
def register(chi)
|
|
62
|
-
chi
|
|
78
|
+
# An older chi has no judgments; the check-in itself is the same there.
|
|
79
|
+
@declared = chi.respond_to?(:judgment)
|
|
80
|
+
if @declared
|
|
81
|
+
chi.judgment PROGRESS, questions: { PROGRESS => { type: "noul", instructions: PROGRESS_QUESTION } },
|
|
82
|
+
shared_host: :skip
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
chi.on(:before_turn) { |event, ctx| before_turn(event, ctx) }
|
|
63
86
|
chi.on(:after_tool_call) { |event, ctx| after_tool_call(event, ctx) }
|
|
64
87
|
chi.on(:after_turn) { |event, ctx| after_turn(event, ctx) }
|
|
65
88
|
chi.command "/checkin", "check on a long turn: status, on|off, <calls>, mode ask|nudge|notify, nudge, later, stop",
|
|
@@ -82,13 +105,21 @@ class Plugin
|
|
|
82
105
|
@nudge = nil # the card's Nudge this turn: { text:, count: }
|
|
83
106
|
@checked_in = nil # the count at this turn's latest check-in
|
|
84
107
|
@nudged_for = nil # the check-in (its count, :none before one) nudged
|
|
108
|
+
@prompt = nil # the owner's prompt, for the judgment
|
|
109
|
+
@calls_since = [] # the calls since the last check-in, for the judgment
|
|
110
|
+
@calls_bytes = 0
|
|
111
|
+
@run = nil # the latest check-in's run, for its labels
|
|
85
112
|
end
|
|
86
113
|
|
|
87
|
-
def before_turn(ctx)
|
|
114
|
+
def before_turn(event, ctx)
|
|
88
115
|
close_card(ctx, "The turn ended after #{calls} tool calls.")
|
|
89
116
|
@mutex.synchronize do
|
|
90
117
|
load_session(ctx)
|
|
91
118
|
reset
|
|
119
|
+
# A continue, a wake or a reminder has no prompt of its own, and a
|
|
120
|
+
# participant's turn says nothing about the owner's: the judgment gets
|
|
121
|
+
# none then.
|
|
122
|
+
@prompt = ctx.turn_author ? nil : event[:prompt]
|
|
92
123
|
end
|
|
93
124
|
end
|
|
94
125
|
|
|
@@ -100,13 +131,17 @@ class Plugin
|
|
|
100
131
|
load_session(ctx)
|
|
101
132
|
@count += 1
|
|
102
133
|
@tools = (@tools + [tool]).last(LAST_TOOLS)
|
|
134
|
+
note_call(event)
|
|
103
135
|
next nil unless @enabled && @count >= @next_at
|
|
104
136
|
|
|
105
137
|
@next_at = @count + @every
|
|
106
138
|
@checked_in = @count
|
|
107
139
|
[@count, @mode]
|
|
108
140
|
end
|
|
109
|
-
|
|
141
|
+
return unless due
|
|
142
|
+
|
|
143
|
+
check_in(ctx, *due)
|
|
144
|
+
ask_progress(ctx)
|
|
110
145
|
end
|
|
111
146
|
|
|
112
147
|
def check_in(ctx, count, mode)
|
|
@@ -128,7 +163,7 @@ class Plugin
|
|
|
128
163
|
end
|
|
129
164
|
|
|
130
165
|
def after_turn(event, ctx)
|
|
131
|
-
|
|
166
|
+
label(ctx, "turn_stopped", by: "turn", strength: nil) if close_card(ctx, "The turn ended after #{calls} tool calls.")
|
|
132
167
|
|
|
133
168
|
nudge_not_sent(event, ctx)
|
|
134
169
|
end
|
|
@@ -191,6 +226,63 @@ class Plugin
|
|
|
191
226
|
true
|
|
192
227
|
end
|
|
193
228
|
|
|
229
|
+
# --- the progress judgment (audit) ---------------------------------------
|
|
230
|
+
|
|
231
|
+
# Every counted call since the last check-in, in the words System 1 sees:
|
|
232
|
+
# the tool, what it was asked for, what it said. Cut here, so the state
|
|
233
|
+
# stays near 4 KB whatever a tool returned (docs/system-one.md: this is
|
|
234
|
+
# tool output reaching System 1, which approvals and context never send).
|
|
235
|
+
def note_call(event)
|
|
236
|
+
entry = "#{event[:tool]} #{cut(call_args(event[:call]), CALL_CHARS)} -> #{cut(event[:result] || event[:output], RESULT_CHARS)}"
|
|
237
|
+
@calls_since << entry
|
|
238
|
+
@calls_bytes += entry.bytesize
|
|
239
|
+
@calls_bytes -= @calls_since.shift.bytesize while @calls_bytes > CALLS_TOTAL && @calls_since.size > 1
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def call_args(call)
|
|
243
|
+
args = call.is_a?(Hash) ? (call[:arguments] || call["arguments"] || call) : call
|
|
244
|
+
return args if args.is_a?(String)
|
|
245
|
+
|
|
246
|
+
# The tool's name is already in the entry and nil fields say nothing:
|
|
247
|
+
# the cut should keep what the call asked for.
|
|
248
|
+
args = args.compact.except(:name, "name") if args.is_a?(Hash)
|
|
249
|
+
JSON.generate(args)
|
|
250
|
+
rescue StandardError
|
|
251
|
+
args.to_s
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def cut(text, limit)
|
|
255
|
+
text = text.to_s
|
|
256
|
+
text.length > limit ? "#{text[0, limit]}…" : text
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# The ask, on its own thread. Nothing reads the answer: the turn and the
|
|
260
|
+
# card are what they are with the judgment off. `shared_host: :skip` (the
|
|
261
|
+
# declaration) saves the run with `asked: false` when a turn is running on
|
|
262
|
+
# the same local System 1 host, so the ask never evicts that turn's cache.
|
|
263
|
+
def ask_progress(ctx)
|
|
264
|
+
return unless @declared
|
|
265
|
+
|
|
266
|
+
state = @mutex.synchronize do
|
|
267
|
+
state = { prompt: @prompt, calls: @count, elapsed_s: (monotonic - @started).round, tools: @calls_since.dup }
|
|
268
|
+
@calls_since = []
|
|
269
|
+
@calls_bytes = 0
|
|
270
|
+
state
|
|
271
|
+
end
|
|
272
|
+
run = ctx.judge_later(PROGRESS, state: state)
|
|
273
|
+
@mutex.synchronize { @run = run }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# What the user did with the card, saved on this turn's latest check-in's
|
|
277
|
+
# run: raw words, mapped only at analysis, since a Nudge asks for a status
|
|
278
|
+
# rather than a verdict and Keep going may be habit. A run that was never
|
|
279
|
+
# saved (the judgment off, no host) labels nothing.
|
|
280
|
+
def label(ctx, value, by: "card", strength: "weak")
|
|
281
|
+
return false unless @declared
|
|
282
|
+
|
|
283
|
+
ctx.judgments.label(@mutex.synchronize { @run }, value, by: by, strength: strength)
|
|
284
|
+
end
|
|
285
|
+
|
|
194
286
|
# --- /checkin --------------------------------------------------------------
|
|
195
287
|
|
|
196
288
|
def command(args, ctx)
|
|
@@ -262,6 +354,7 @@ class Plugin
|
|
|
262
354
|
end
|
|
263
355
|
|
|
264
356
|
@mutex.synchronize { @nudge = { text: text, count: count } }
|
|
357
|
+
label(ctx, "nudge") if card_open?
|
|
265
358
|
close_card(ctx, "Nudged the model at #{count} tool calls.")
|
|
266
359
|
nil
|
|
267
360
|
end
|
|
@@ -270,12 +363,14 @@ class Plugin
|
|
|
270
363
|
at = @mutex.synchronize { @next_at }
|
|
271
364
|
return "check-in: no check-in open" unless close_card(ctx, "Kept going; the next check-in is at #{at} tool calls.")
|
|
272
365
|
|
|
366
|
+
label(ctx, "keep_going")
|
|
273
367
|
nil
|
|
274
368
|
end
|
|
275
369
|
|
|
276
370
|
def stop(ctx)
|
|
277
371
|
return "check-in: no turn running" unless ctx.stop_turn("stopped from check-in")
|
|
278
372
|
|
|
373
|
+
label(ctx, "stop") if card_open?
|
|
279
374
|
close_card(ctx, "Stopped the turn at #{calls} tool calls.")
|
|
280
375
|
nil
|
|
281
376
|
end
|
|
@@ -326,6 +421,8 @@ class Plugin
|
|
|
326
421
|
|
|
327
422
|
def calls = @mutex.synchronize { @count }
|
|
328
423
|
|
|
424
|
+
def card_open? = @mutex.synchronize { @card_open }
|
|
425
|
+
|
|
329
426
|
def message(count) = @message.gsub("{calls}", count.to_s)
|
|
330
427
|
|
|
331
428
|
def elapsed
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: core
|
|
3
|
-
version: 0.4.
|
|
3
|
+
version: 0.4.1
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Recommended safety set"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
# Its members' highest floor (work-cap's: ctx.usage, raised at the release)
|
|
8
|
-
requires_chi: ">= 0.
|
|
8
|
+
requires_chi: ">= 0.142.0"
|
|
9
9
|
includes:
|
|
10
10
|
- loop-guard
|
|
11
11
|
- check-in
|
|
@@ -16,9 +16,9 @@ Single registry `Samagotchi::Config` (`Config::ENTRIES` in `lib/samagotchi/confi
|
|
|
16
16
|
- **YAML**: `nested: { param: value }` → dotted `nested.param` (lower `snake_case`, leaf keeps `_`; kebab alias `base-url` also accepted and normalized)
|
|
17
17
|
- **CLI**: `--nested-param` (kebab, `_` → `-` for both section+leaf; `--nested_param` is rejected as unknown by `chi` with a "did you mean" hint)
|
|
18
18
|
|
|
19
|
-
Sections forbid `_`/`-` (`SECTION_RE` `/\A[a-z0-9]+\z/`);
|
|
19
|
+
Sections forbid `_`/`-` (`SECTION_RE` `/\A[a-z0-9]+\z/`), since an env or CLI name is derived from them; a config-only section (`system_one`: `expose: [:config]`, no derived name) may carry `_`. Leaves keep `snake_case` in YAML (`base_url`) and become kebab in CLI (`base-url`) via registry derivation — no generic string split, registry lookup avoids flat vs nested collision.
|
|
20
20
|
|
|
21
|
-
**Universal entries** (`expose: [:env,:config,:cli]`): `default.model`, `server.host/port/open_timeout/read_timeout`, `server.first_token_timeout` (env and config only), `recap.model/base_url/host_ref/inactivity/timeout/min_user_turns/sentences`, `session.retention_days/max_count/keep_status/sweep_interval_hours/idle_exit_minutes`, `session.shared/keep_empty/max_children` (env and config only), `log.file/disable`, `status.line`, `context.status/window_tokens/chars_per_token/status_thresholds`, `memory.index_warn_tokens`, `default.max_tokens`, `max_tool_output_chars`, `retry.max/base_delay/max_delay`, `read.*`, `execute.*`, `web.port/host`, `no_interrupt`, `no_default_input` etc. (`Config::ENTRIES`; `expose` says which of env/config/cli each takes). Precedence is `CLI > ENV > file > default`.
|
|
21
|
+
**Universal entries** (`expose: [:env,:config,:cli]`): `default.model`, `server.host/port/open_timeout/read_timeout`, `server.first_token_timeout` (env and config only), `recap.model/base_url/host_ref/inactivity/timeout/min_user_turns/sentences`, `session.retention_days/max_count/keep_status/sweep_interval_hours/idle_exit_minutes`, `session.shared/keep_empty/max_children` (env and config only), `log.file/disable`, `status.line`, `context.status/window_tokens/chars_per_token/status_thresholds`, `memory.index_warn_tokens/entry_warn_tokens/read_max_chars`, `default.max_tokens`, `max_tool_output_chars`, `retry.max/base_delay/max_delay`, `read.*`, `execute.*`, `web.port/host`, `no_interrupt`, `no_default_input` etc. (`Config::ENTRIES`; `expose` says which of env/config/cli each takes). Precedence is `CLI > ENV > file > default`.
|
|
22
22
|
|
|
23
23
|
Example `config.yml` (new nested form, preferred):
|
|
24
24
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.142.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:
|
|
9
|
-
config_modification_protocol.md: sha256:
|
|
10
|
-
memory_guide.md: sha256:
|
|
8
|
+
self_map.md: sha256:b97087aff3621127e8519a22987a625798f88308d71c6184b68409b7f6f20570
|
|
9
|
+
config_modification_protocol.md: sha256:e6b55d8703ccce866047b412837c33ee23f34f894cd7a5b65421eee93ee844aa
|
|
10
|
+
memory_guide.md: sha256:64c3fa0703a3bdd8ae33b9e7cb341caa88a205c289d767f4877b38dfa6629fda
|
|
11
11
|
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|
|
@@ -41,7 +41,7 @@ This memory teaches you (the agent) how to use Samagotchi memories — persisten
|
|
|
41
41
|
|
|
42
42
|
**When to write:**
|
|
43
43
|
- After learning a durable preference (e.g. commit style, test command, coding guideline) that the user confirmed or you observed repeatedly — ask before overwriting existing entries where appropriate.
|
|
44
|
-
- Keep entries small and focused (one topic per file). Use clear filenames: `commit_preferences`, `testing_guide`, `project_conventions`.
|
|
44
|
+
- Keep entries compact: `memory_read` returns a memory whole and every read pays its size. A write that takes one over `memory.entry_warn_tokens` (5000 tokens) ends with a note: tighten it when you next have a moment, but never remove or merge memories unless the user asks. Keep entries small and focused (one topic per file). Use clear filenames: `commit_preferences`, `testing_guide`, `project_conventions`.
|
|
45
45
|
- Never store secrets, tokens, or transient state. Memories are shared via bundles. One exception: a skill may keep a working handoff memory (its steps say so) whose status is its description, removed when the work is done.
|
|
46
46
|
|
|
47
47
|
**Placeholders:**
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
approvals (I see only how each was answered); see `docs/sessions.md` "Delegating".
|
|
54
54
|
- Bundles extend me: memories, `hooks/*.rb`, `guardrails/*.yml`, and a `plugin.rb` that adds slash
|
|
55
55
|
commands (`anytime:` ones run mid-turn), model tools (they can return images I see), hooks,
|
|
56
|
-
cards, side answers (`ask_model`), HTTP calls (`ctx.http`),
|
|
56
|
+
cards, side answers (`ask_model`), System 1 judgments (`chi.judgment`, `ctx.judge`), HTTP calls (`ctx.http`),
|
|
57
57
|
child sessions and services (e.g. MCP server processes). Shipped: `btw` (`/btw` side question),
|
|
58
58
|
`mcp` (MCP server tools, searched with find_tools and called with call_tool; a screenshot comes as a picture; a toolkit's undeclared built-ins too, as chi/<tool>), `guardrails` (rules), `known-names` (typo guard), `source-links`
|
|
59
59
|
(turns source refs like JIRA-123 in an answer into links in the web, and a one-line note), `loop-guard`
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -179,17 +179,21 @@ module Samagotchi
|
|
|
179
179
|
# mode GuardrailWiring also reads rules, threshold and min_record for the auto-allow (docs/guardrails.md).
|
|
180
180
|
# off (the default) | audit (judge every ask, log it, never decide) | on. An unknown value warns and is off.
|
|
181
181
|
Entry.new(key: "guardrails.system_one.mode", yaml_path: %w[guardrails system_one mode], type: :enum, default: "off", expose: %i[config], enum_values: %w[off audit on], lifecycle: :next_tool_or_approval),
|
|
182
|
-
# The hosts: entry serving /v1/systemone (Splash, or the hosted Jev API); unset: no judgment.
|
|
183
|
-
Entry.new(key: "guardrails.system_one.host", yaml_path: %w[guardrails system_one host], type: :string, default: nil, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
184
|
-
# The judgment model; unset: the host's first model, and no judgment if it names none.
|
|
185
|
-
Entry.new(key: "guardrails.system_one.model", yaml_path: %w[guardrails system_one model], type: :string, default: nil, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
186
182
|
# The rule ids System 1 may answer for ("a|b", or a YAML list, parsed like guardrails.small_models).
|
|
187
183
|
Entry.new(key: "guardrails.system_one.rules", yaml_path: %w[guardrails system_one rules], type: :string, default: nil, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
188
184
|
# The probability an auto-allow needs, and how many clean records a rule needs before it is trusted.
|
|
189
185
|
Entry.new(key: "guardrails.system_one.threshold", yaml_path: %w[guardrails system_one threshold], type: :float, default: 0.8, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
190
186
|
Entry.new(key: "guardrails.system_one.min_record", yaml_path: %w[guardrails system_one min_record], type: :integer, default: 5, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
|
|
188
|
+
# What every use of System 1 shares: approvals, toolkit suggestions, context candidates and bundles' judgments.
|
|
189
|
+
# The hosts: entry serving /v1/systemone (Splash, or the hosted Jev API); unset: no judgment, so nothing auto-allows.
|
|
190
|
+
Entry.new(key: "system_one.host", yaml_path: %w[system_one host], type: :string, default: nil, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
191
|
+
# The judgment model; unset: the host's first model, and no judgment if it names none.
|
|
192
|
+
Entry.new(key: "system_one.model", yaml_path: %w[system_one model], type: :string, default: nil, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
193
|
+
# How long one judgment may take, in milliseconds; a judgment that doesn't arrive is no judgment.
|
|
194
|
+
Entry.new(key: "system_one.timeout_ms", yaml_path: %w[system_one timeout_ms], type: :integer, default: 2000, expose: %i[config], lifecycle: :next_tool_or_approval),
|
|
195
|
+
# Save each System 1 ask and what came of it (docs/system-one.md): on (the default) | off.
|
|
196
|
+
Entry.new(key: "system_one.runs", yaml_path: %w[system_one runs], type: :enum, default: "on", expose: %i[config], enum_values: %w[on off], lifecycle: :next_tool_or_approval),
|
|
193
197
|
|
|
194
198
|
Entry.new(key: "log.file", yaml_path: %w[log file], type: :string, default: nil, expose: %i[env config cli], lifecycle: :process_start),
|
|
195
199
|
Entry.new(key: "log.disable", yaml_path: %w[log disable], type: :bool, default: false, expose: %i[env config cli], lifecycle: :process_start),
|
|
@@ -204,6 +208,12 @@ module Samagotchi
|
|
|
204
208
|
# The tokens one scope's memory index (sent with every prompt) may hold before a write that crosses it gets a note
|
|
205
209
|
# (MemoryBundle::IndexSize); 0: no note. `memory:` is its own section, not the `memories:` preload list.
|
|
206
210
|
Entry.new(key: "memory.index_warn_tokens", yaml_path: %w[memory index_warn_tokens], type: :integer, default: 2500, expose: %i[env config cli], lifecycle: :next_matching_turn_display_poll_write),
|
|
211
|
+
# The most characters one memory_read result holds (ToolOutputCut cuts it with a line naming the memory file); 0: no
|
|
212
|
+
# fixed cap. memory_read is not cut by max_tool_output_chars (ToolRunner).
|
|
213
|
+
Entry.new(key: "memory.read_max_chars", yaml_path: %w[memory read_max_chars], type: :integer, default: 0, expose: %i[env config cli], lifecycle: :next_matching_use),
|
|
214
|
+
# The tokens one memory may hold (each read of it pays them) before a write that crosses it gets a note
|
|
215
|
+
# (MemoryBundle::EntrySize); 0: no note.
|
|
216
|
+
Entry.new(key: "memory.entry_warn_tokens", yaml_path: %w[memory entry_warn_tokens], type: :integer, default: 5000, expose: %i[env config cli], lifecycle: :next_matching_turn_display_poll_write),
|
|
207
217
|
Entry.new(key: "context.status_thresholds", yaml_path: %w[context status_thresholds], type: :string, default: "20,40,60,80", expose: %i[env config cli], lifecycle: :next_matching_turn_display_poll_write),
|
|
208
218
|
# Attached context (ContextSources): how often a source's command runs when it names no --every (seconds; less
|
|
209
219
|
# than 30 counts as 30).
|
|
@@ -219,7 +229,7 @@ module Samagotchi
|
|
|
219
229
|
Entry.new(key: "context.peer_turns", yaml_path: %w[context peer_turns], type: :integer, default: 3, expose: %i[env config cli], lifecycle: :next_matching_turn_display_poll_write),
|
|
220
230
|
# The candidate judge (ContextJudge): off (no conversation text goes to a System 1 host); audit (asks and logs
|
|
221
231
|
# context_candidates.jsonl, attaches nothing); on (ContextCandidateChoice: p ≥ threshold attaches quietly,
|
|
222
|
-
# offer_threshold ≤ p < threshold shows one card). Its host and model are
|
|
232
|
+
# offer_threshold ≤ p < threshold shows one card). Its host and model are system_one.host and .model.
|
|
223
233
|
# reask_after_turns: the owner prompts after a judged ask before its one re-ask.
|
|
224
234
|
Entry.new(key: "context.candidates.mode", yaml_path: %w[context candidates mode], type: :enum, default: "off", expose: %i[config], enum_values: %w[off audit on], lifecycle: :next_matching_turn_display_poll_write),
|
|
225
235
|
Entry.new(key: "context.candidates.threshold", yaml_path: %w[context candidates threshold], type: :float, default: 0.8, expose: %i[config], lifecycle: :next_matching_turn_display_poll_write),
|
|
@@ -260,7 +270,7 @@ module Samagotchi
|
|
|
260
270
|
Entry.new(key: "toolkit.default", yaml_path: %w[toolkit default], type: :string, default: nil, expose: %i[env config], lifecycle: :session_creation),
|
|
261
271
|
Entry.new(key: "toolkit.child", yaml_path: %w[toolkit child], type: :string, default: nil, expose: %i[env config], lifecycle: :session_creation),
|
|
262
272
|
# Toolkit suggestion (ToolkitSuggest): off; audit (logs, never blocks or changes a session); on (the first eligible
|
|
263
|
-
# turn waits up to
|
|
273
|
+
# turn waits up to system_one.timeout_ms and may start on the suggested kit).
|
|
264
274
|
Entry.new(key: "toolkit.suggest.mode", yaml_path: %w[toolkit suggest mode], type: :enum, default: "off", expose: %i[config], enum_values: %w[off audit on], lifecycle: :worker_start),
|
|
265
275
|
Entry.new(key: "toolkit.suggest.threshold", yaml_path: %w[toolkit suggest threshold], type: :float, default: 0.5, expose: %i[config], lifecycle: :worker_start),
|
|
266
276
|
Entry.new(key: "toolkit.suggest.defer_turns", yaml_path: %w[toolkit suggest defer_turns], type: :integer, default: 3, expose: %i[config], lifecycle: :worker_start),
|
|
@@ -390,8 +400,18 @@ module Samagotchi
|
|
|
390
400
|
# The keys a hosts.<name>.models entry may hold (HostModel.parse_map).
|
|
391
401
|
HOST_MODEL_KEYS = %w[price served].freeze
|
|
392
402
|
# Section keys beyond the registry's: guardrails' YAML rules (Engine#guardrail_rules).
|
|
393
|
-
# And toolkit's kits (Toolkit.kits).
|
|
394
|
-
SECTION_EXTRA_KEYS = { "guardrails" => %w[rules disable].freeze, "toolkit" => %w[kits].freeze }.freeze
|
|
403
|
+
# And toolkit's kits (Toolkit.kits), and bundles' judgments' settings (SystemOne::JudgmentSettings).
|
|
404
|
+
SECTION_EXTRA_KEYS = { "guardrails" => %w[rules disable].freeze, "toolkit" => %w[kits].freeze, "system_one" => %w[judgments].freeze }.freeze
|
|
405
|
+
|
|
406
|
+
# guardrails.system_one's keys that moved to system_one (SystemOne::Client.from_config).
|
|
407
|
+
SYSTEM_ONE_MOVED_KEYS = %w[host model timeout_ms].freeze
|
|
408
|
+
# What a system_one.judgments entry may hold. Core's judgments take none (RESERVED_JUDGMENTS).
|
|
409
|
+
JUDGMENT_ENTRY_KEYS = %w[mode host model].freeze
|
|
410
|
+
JUDGMENT_MODES = %w[off audit on].freeze
|
|
411
|
+
# Names core keeps, as a judgment's first word: a bundle can't declare one and
|
|
412
|
+
# system_one.judgments can't set one (Record and Similar are model-blind, so a
|
|
413
|
+
# swapped model would inherit a clean record and could auto-allow).
|
|
414
|
+
RESERVED_JUDGMENTS = %w[approvals context guardrails toolkit].freeze
|
|
395
415
|
|
|
396
416
|
# What a :bool value reads as (Config.coerce!), YAML 1.1's words and 1/0.
|
|
397
417
|
BOOL_WORDS = { "1" => true, "true" => true, "yes" => true, "on" => true,
|
|
@@ -464,6 +484,14 @@ module Samagotchi
|
|
|
464
484
|
word && entry.enum_values.include?(word) ? word : nil
|
|
465
485
|
end
|
|
466
486
|
|
|
487
|
+
# A system_one.judgments entry's mode as written, nil for one it can't
|
|
488
|
+
# be: an unquoted on/off is a boolean there too.
|
|
489
|
+
# @return [String, nil]
|
|
490
|
+
def judgment_mode(raw)
|
|
491
|
+
word = BOOL_ENUM_WORDS.fetch(raw) { raw.to_s.strip.downcase }
|
|
492
|
+
word if JUDGMENT_MODES.include?(word)
|
|
493
|
+
end
|
|
494
|
+
|
|
467
495
|
# What +entry+'s type takes, as a refusal names it.
|
|
468
496
|
def takes(entry)
|
|
469
497
|
case entry.type
|
|
@@ -692,6 +720,8 @@ module Samagotchi
|
|
|
692
720
|
# A section's leaf: a key the registry knows, or a mapping of a
|
|
693
721
|
# third level (web.participants.cap) whose keys it knows.
|
|
694
722
|
def leaf_problems(path, value)
|
|
723
|
+
return judgment_problems(value) if path == "system_one.judgments"
|
|
724
|
+
|
|
695
725
|
if value.is_a?(Hash) && ENTRIES.any? { |e| e.key.start_with?("#{path}.") }
|
|
696
726
|
value.keys.filter_map { |leaf| key_problem("#{path}.#{leaf}") }
|
|
697
727
|
else
|
|
@@ -699,6 +729,29 @@ module Samagotchi
|
|
|
699
729
|
end
|
|
700
730
|
end
|
|
701
731
|
|
|
732
|
+
# system_one.judgments' entries: a map entry bypasses the registry, so
|
|
733
|
+
# its keys and mode are checked here (an unquoted on/off is a boolean).
|
|
734
|
+
# An undeclared name can only be told once the plugins have loaded
|
|
735
|
+
# (SystemOne::Judgments#warn_undeclared).
|
|
736
|
+
def judgment_problems(value)
|
|
737
|
+
return value.nil? ? [] : ["config: 'system_one.judgments' must be a mapping of judgments; ignored"] unless value.is_a?(Hash)
|
|
738
|
+
|
|
739
|
+
value.flat_map do |name, entry|
|
|
740
|
+
where = "system_one.judgments.#{name}"
|
|
741
|
+
if RESERVED_JUDGMENTS.include?(name.to_s.split(".").first)
|
|
742
|
+
next ["config: #{where}: core's judgments take no settings of their own; ignored (see docs/system-one.md)"]
|
|
743
|
+
end
|
|
744
|
+
next ["config: #{where} must be a mapping of #{JUDGMENT_ENTRY_KEYS.join(", ")}; ignored"] unless entry.is_a?(Hash)
|
|
745
|
+
|
|
746
|
+
unknown = (entry.keys.map(&:to_s) - JUDGMENT_ENTRY_KEYS).map do |k|
|
|
747
|
+
unknown_key_message("#{where}.#{k}", k, JUDGMENT_ENTRY_KEYS, prefix: "#{where}.")
|
|
748
|
+
end
|
|
749
|
+
mode = entry.key?("mode") && judgment_mode(entry["mode"]).nil? ? ["config: #{where}.mode must be off, audit or on; it is off"] : []
|
|
750
|
+
text = %w[host model].reject { |k| entry[k].nil? || entry[k].is_a?(String) }.map { |k| "config: #{where}.#{k} must be text" }
|
|
751
|
+
unknown + mode + text
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
|
|
702
755
|
# What a model ref in config.yml won't do as written: an alias whose
|
|
703
756
|
# target is another alias (aliases apply once, ModelRef), a ref that
|
|
704
757
|
# starts with "<host>/" (only ':' names a host).
|
|
@@ -752,10 +805,17 @@ module Samagotchi
|
|
|
752
805
|
@known_sections ||= known_config_keys.filter_map { |k| k.split(".").first if k.include?(".") }.to_set.freeze
|
|
753
806
|
end
|
|
754
807
|
|
|
755
|
-
# Keys replaced by another
|
|
808
|
+
# Keys replaced by another, and what the warning adds: it names the new
|
|
809
|
+
# one (no alias).
|
|
810
|
+
PARENT_ANSWERS_HINT = "hard, standard or soft; see docs/guardrails.md"
|
|
811
|
+
SYSTEM_ONE_MOVED_HINT = "it has moved: until all of host, model and timeout_ms have, nothing is judged or " \
|
|
812
|
+
"auto-allowed, and every ask goes to you; see docs/configuration.md"
|
|
756
813
|
REPLACED_KEYS = {
|
|
757
|
-
"guardrails.parent_approvals" => "guardrails.parent_answers",
|
|
758
|
-
"turn.parent_continue" => "guardrails.parent_answers"
|
|
814
|
+
"guardrails.parent_approvals" => ["guardrails.parent_answers", PARENT_ANSWERS_HINT],
|
|
815
|
+
"turn.parent_continue" => ["guardrails.parent_answers", PARENT_ANSWERS_HINT],
|
|
816
|
+
"guardrails.system_one.host" => ["system_one.host", SYSTEM_ONE_MOVED_HINT],
|
|
817
|
+
"guardrails.system_one.model" => ["system_one.model", SYSTEM_ONE_MOVED_HINT],
|
|
818
|
+
"guardrails.system_one.timeout_ms" => ["system_one.timeout_ms", SYSTEM_ONE_MOVED_HINT]
|
|
759
819
|
}.freeze
|
|
760
820
|
|
|
761
821
|
# nil when +dotted+ is a key the file may hold.
|
|
@@ -763,7 +823,7 @@ module Samagotchi
|
|
|
763
823
|
return nil if known_config_keys.include?(dotted)
|
|
764
824
|
return "config: #{ConfigFile.removed_transport_problem("'#{dotted}'")}" if dotted == "server.transport"
|
|
765
825
|
if (replaced = REPLACED_KEYS[dotted])
|
|
766
|
-
return "config: unknown key '#{dotted}': replaced by '#{replaced}' (
|
|
826
|
+
return "config: unknown key '#{dotted}': replaced by '#{replaced.first}' (#{replaced.last})"
|
|
767
827
|
end
|
|
768
828
|
|
|
769
829
|
entry = find_by_key(dotted)
|
|
@@ -133,12 +133,19 @@ module Samagotchi
|
|
|
133
133
|
own.add(ContextCandidates.source_for(candidate, why: CHOSEN_WHY))
|
|
134
134
|
Log.info(:context, "candidate_attached", name: candidate.name, trigger: "command")
|
|
135
135
|
ContextCandidatesLog.event("attached", session_id: session.id, name: candidate.name, state_dir: state_dir, by: "command")
|
|
136
|
+
label_run(session, candidate, "attached", state_dir)
|
|
136
137
|
show_card.call(**closed_card(candidate, "Attached."))
|
|
137
138
|
[:ok, "attached #{candidate.name}: its text comes with your next message"]
|
|
138
139
|
rescue ContextSources::Invalid, ContextProviders::Invalid => e
|
|
139
140
|
[:error, "#{candidate.name} not attached: #{e.message}"]
|
|
140
141
|
end
|
|
141
142
|
|
|
143
|
+
# /context attach|decline (the card's buttons), on the latest ask's run.
|
|
144
|
+
def label_run(session, candidate, value, state_dir)
|
|
145
|
+
records = ContextCandidatesLog.records_for(ContextCandidatesLog.log_for(state_dir), session.id)
|
|
146
|
+
ContextCandidatesLog.label_run(records, candidate.name, value, by: "command", sessions_dir: state_dir)
|
|
147
|
+
end
|
|
148
|
+
|
|
142
149
|
def decline(candidate, status, session, state_dir, show_card)
|
|
143
150
|
if status == "attached"
|
|
144
151
|
return [:error, "#{candidate.name} is attached; chi context rm #{candidate.name} removes it"]
|
|
@@ -147,6 +154,7 @@ module Samagotchi
|
|
|
147
154
|
|
|
148
155
|
ContextSources.session_location(session.id, state_dir: state_dir).decline(candidate.name, candidate.url.to_s)
|
|
149
156
|
ContextCandidatesLog.event("declined", session_id: session.id, name: candidate.name, state_dir: state_dir, by: "command")
|
|
157
|
+
label_run(session, candidate, "declined", state_dir)
|
|
150
158
|
show_card.call(**closed_card(candidate, "Not attached here."))
|
|
151
159
|
[:ok, "declined #{candidate.name}: not proposed again in this session"]
|
|
152
160
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "log"
|
|
4
4
|
require_relative "session"
|
|
5
5
|
require_relative "side_judgments"
|
|
6
|
+
require_relative "system_one/runs"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
# The candidates' record, `context_candidates.jsonl` in the state dir
|
|
@@ -15,7 +16,8 @@ module Samagotchi
|
|
|
15
16
|
# Kinds (every line has session_id, name and at):
|
|
16
17
|
# - proposed: a plugin's ctx.context.propose recorded it (title, by)
|
|
17
18
|
# - mentioned: an owner prompt named it and it was attached (mention)
|
|
18
|
-
# - judged: System 1's ask (ContextJudge.ask: p, task_p, stated,
|
|
19
|
+
# - judged: System 1's ask (ContextJudge.ask: run_id, p, task_p, stated,
|
|
20
|
+
# would, …)
|
|
19
21
|
# - judge_attached: on mode attached it (p ≥ threshold; p)
|
|
20
22
|
# - carded: on mode showed its card (the middle band; p): once per
|
|
21
23
|
# candidate, read back by ContextCandidateChoice.carded?
|
|
@@ -34,6 +36,10 @@ module Samagotchi
|
|
|
34
36
|
STATUS_OF = { "proposed" => "proposed", "mentioned" => "attached", "judge_attached" => "attached", "attached" => "attached",
|
|
35
37
|
"declined" => "declined", "detached" => "proposed", "withdrawn" => nil }.freeze
|
|
36
38
|
|
|
39
|
+
# The kinds #look sees that are a removal: a chip's ×, chi context rm. One
|
|
40
|
+
# removed after the session's last turn is never seen.
|
|
41
|
+
REMOVED = %w[declined detached].freeze
|
|
42
|
+
|
|
37
43
|
module_function
|
|
38
44
|
|
|
39
45
|
# @param state_dir [String, nil] the sessions folder (Session.default_state_dir)
|
|
@@ -77,10 +83,28 @@ module Samagotchi
|
|
|
77
83
|
|
|
78
84
|
kind = status == "proposed" ? "detached" : status
|
|
79
85
|
event(kind, session_id: session_id, name: candidate.name, log: log, by: "user")
|
|
86
|
+
label_run(records, candidate.name, kind, by: "user", root: log.dir) if REMOVED.include?(kind)
|
|
80
87
|
kind
|
|
81
88
|
end
|
|
82
89
|
end
|
|
83
90
|
|
|
91
|
+
# The user's say on a candidate, as a label on the latest ask about it
|
|
92
|
+
# (the judged line's run, question "about"). No ask with a run: nothing.
|
|
93
|
+
# Never raises.
|
|
94
|
+
# @param records [Array<Hash>] the session's lines (#records_for)
|
|
95
|
+
# @param root [String, nil] the state dir; nil: from +sessions_dir+
|
|
96
|
+
# @return [Boolean] whether a label was written
|
|
97
|
+
def label_run(records, name, value, by:, sessions_dir: nil, root: nil)
|
|
98
|
+
judged = records.reverse_each.find { |line| line["kind"] == "judged" && line["name"] == name && line["run_id"] }
|
|
99
|
+
return false if judged.nil?
|
|
100
|
+
|
|
101
|
+
runs = SystemOne::Runs.in(root || File.dirname(sessions_dir || Session.default_state_dir))
|
|
102
|
+
runs.label(judged["run_id"], value, by: by, question: "about")
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
Log.warn(:context, "context_candidates_log_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
105
|
+
false
|
|
106
|
+
end
|
|
107
|
+
|
|
84
108
|
# +text+ as a log line keeps it: one line, at most PROMPT_MAX chars.
|
|
85
109
|
def short(text)
|
|
86
110
|
line = text.to_s.gsub(/\s+/, " ").strip
|
|
@@ -4,6 +4,7 @@ require_relative "config"
|
|
|
4
4
|
require_relative "context_candidates_log"
|
|
5
5
|
require_relative "log"
|
|
6
6
|
require_relative "system_one/question"
|
|
7
|
+
require_relative "system_one/runs"
|
|
7
8
|
|
|
8
9
|
module Samagotchi
|
|
9
10
|
# System 1's judgment of a session's candidate context
|
|
@@ -26,6 +27,8 @@ module Samagotchi
|
|
|
26
27
|
# (attach, card, or nothing); audit only logs.
|
|
27
28
|
module ContextJudge
|
|
28
29
|
MODES = %w[off audit on].freeze
|
|
30
|
+
# Its saved asks' name (SystemOne::Runs).
|
|
31
|
+
JUDGMENT = "context.candidate"
|
|
29
32
|
ABOUT_ID = "about"
|
|
30
33
|
TASK_ID = "task"
|
|
31
34
|
# The bound on each prompt in the state.
|
|
@@ -168,14 +171,15 @@ module Samagotchi
|
|
|
168
171
|
# candidate is asked again at the next owner prompt.
|
|
169
172
|
# @return [Hash, nil] the line's fields
|
|
170
173
|
def ask(client:, job:, log:, settings:, cancel_controller: nil)
|
|
174
|
+
run_id = SystemOne::Runs.new_id(JUDGMENT)
|
|
171
175
|
answers = client.ask(state: state(job), questions: questions(child: !job.parent_attached.nil?),
|
|
172
|
-
cancel_controller: cancel_controller)
|
|
176
|
+
cancel_controller: cancel_controller, run_id: run_id)
|
|
173
177
|
about = answers&.dig(ABOUT_ID)&.noul
|
|
174
178
|
task = answers&.dig(TASK_ID)&.noul
|
|
175
179
|
return nil if about.nil? || task.nil?
|
|
176
180
|
|
|
177
181
|
stated = task >= TASK_MIN
|
|
178
|
-
fields = { "title" => job.title, "phase" => job.phase, "owner_prompts" => job.owner_prompts,
|
|
182
|
+
fields = { "run_id" => run_id, "title" => job.title, "phase" => job.phase, "owner_prompts" => job.owner_prompts,
|
|
179
183
|
"p" => about.round(4), "task_p" => task.round(4), "stated" => stated,
|
|
180
184
|
"would" => stated ? would(about, settings) : nil, "mode" => settings.mode,
|
|
181
185
|
"threshold" => settings.threshold, "offer_threshold" => settings.offer_threshold,
|