kairos-chain 3.90.0 → 3.92.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 +107 -0
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/agent/bin/agent_distill.rb +124 -0
- data/templates/skillsets/agent/bin/agent_rule.rb +125 -16
- data/templates/skillsets/agent/bin/agent_shadow_judge.rb +70 -0
- data/templates/skillsets/agent/lib/agent/act_classification.rb +20 -6
- data/templates/skillsets/agent/lib/agent/adoption.rb +340 -0
- data/templates/skillsets/agent/lib/agent/answer_ruling.rb +14 -4
- data/templates/skillsets/agent/lib/agent/delegation.rb +372 -0
- data/templates/skillsets/agent/lib/agent/delegation_table.yml +44 -0
- data/templates/skillsets/agent/lib/agent/distill_run.rb +197 -0
- data/templates/skillsets/agent/lib/agent/distiller.rb +313 -0
- data/templates/skillsets/agent/lib/agent/distiller_prompt.rb +130 -0
- data/templates/skillsets/agent/lib/agent/point_facts.rb +231 -0
- data/templates/skillsets/agent/lib/agent/shadow_judge.rb +564 -0
- data/templates/skillsets/agent/lib/agent.rb +7 -0
- data/templates/skillsets/agent/plugin/SKILL.md +57 -0
- data/templates/skillsets/agent/test/test_agent_distillation.rb +539 -0
- data/templates/skillsets/agent/test/test_agent_shadow_judge.rb +1233 -0
- data/templates/skillsets/agent/tools/agent_status.rb +8 -0
- data/templates/skillsets/agent/tools/agent_step.rb +310 -10
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cea3a83409113bb9513f6da35f8e202ed84762f726997b55d4c0807e3764565
|
|
4
|
+
data.tar.gz: e88354debd6836834ffcf28b905f361b4e9d0d154d22d68f6112a20076fc45c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 811debe565ba6b2815b24860edc7d43a1cec1f3c0da9a8b4d4aed0c52d88052ddbd8f53b544575b253860da2ad669d197f9c5a105ffdc08a8d8e9fb002aade2b
|
|
7
|
+
data.tar.gz: 9682dc5d58a0264960323374db889ad4074e0c063685a0062866e811ec04eb1c91a94fef720a4260b58f69d26f12f9640087213c713dc8c9c1438fb0f836089f
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,113 @@ All notable changes to the `kairos-chain` gem will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [3.92.0] - 2026-10-13
|
|
8
|
+
|
|
9
|
+
### Added — agent: judgment precedents the shadow judge reads, distilled from your answers
|
|
10
|
+
|
|
11
|
+
The shadow judge is a fresh model at every point, so it does not improve with
|
|
12
|
+
use. It can now also read an instance L1 entry, `agent_judgment_precedents`,
|
|
13
|
+
that generalizes your terminal answers and their reasons. The entry is drafted
|
|
14
|
+
by a separate model and reaches the judge only after you adopt it at the
|
|
15
|
+
terminal. **The judge's answer still changes nothing**; every row stays
|
|
16
|
+
`shadow`.
|
|
17
|
+
|
|
18
|
+
- **What the driver records**: when an answer at either point commits, the
|
|
19
|
+
plans it covered are filed in the session (`point_facts.jsonl`), fixed at
|
|
20
|
+
the moment you answered (an approve at an autonomous checkpoint never
|
|
21
|
+
covers the cycles it starts): each step's tool name and effect where the
|
|
22
|
+
act-route table classified it, counts elsewhere. A step the table does not classify is never named. The ruling
|
|
23
|
+
carries the digest of the filed line.
|
|
24
|
+
- **Drafting**: after ten new answers you typed at the terminal with a reason,
|
|
25
|
+
a separate process (`bin/agent_distill.rb`, `claude-opus-5-5` at `xhigh`
|
|
26
|
+
through `llm_call`, sandboxed) drafts the entry again from all your attested
|
|
27
|
+
answers, their terminal reasons, the filed facts and your edits to earlier
|
|
28
|
+
drafts. It never reads the agent's text, the judge's verdicts or an earlier
|
|
29
|
+
entry. Every statement names the answers it rests on, all at one point,
|
|
30
|
+
and its count and that point's totals are computed by code. It runs only while the delegation table is in force, one
|
|
31
|
+
at a time, off the answer path; a failure waits for ten more answers. The
|
|
32
|
+
draft is recorded on the chain (`agent_distill_draft`).
|
|
33
|
+
- **Adoption**: edit the draft's `.edit.md` copy under
|
|
34
|
+
`.kairos/storage/agent_distill/drafts/`, then
|
|
35
|
+
`ruby .kairos/skillsets/agent/bin/agent_rule.rb adopt`. It checks the draft
|
|
36
|
+
against its chain record, refuses while any session is mid-run (checked
|
|
37
|
+
again after you confirm), shows your changes and the difference from the
|
|
38
|
+
entry in force, asks for the nonce, and records one delegation ruling that
|
|
39
|
+
pins exactly the bytes adopted; on any failure or interrupt the previous
|
|
40
|
+
entry is put back. When the entry has left its pin (an interrupted
|
|
41
|
+
adoption, a hand edit), adoption is also the way back. `agent_rule.rb
|
|
42
|
+
distill` drafts now, whatever the count; `agent_rule.rb status` shows the
|
|
43
|
+
count, the last run and waiting drafts.
|
|
44
|
+
- **Only adoption pins new precedents**: any other ruling of the delegation
|
|
45
|
+
table refuses while the entry differs from its last pin.
|
|
46
|
+
- **Counting per version**: `agent_rule.rb shadow` reports agreement apart for
|
|
47
|
+
each combination of the two tables, what the judge read and the judge's own
|
|
48
|
+
prompt (now carried on every seal as `judge_prompt_sha256`). A new adoption
|
|
49
|
+
or an instruction-mode edit starts a new count.
|
|
50
|
+
- The terminal's reason prompt now says that reasons may be distilled into
|
|
51
|
+
this entry, which the agent can read.
|
|
52
|
+
|
|
53
|
+
**After upgrading, rule the delegation table again** where it was in force:
|
|
54
|
+
the shipped table now names `judgment_precedents`, so the judge stops (hash
|
|
55
|
+
mismatch) until you run
|
|
56
|
+
`ruby .kairos/skillsets/agent/bin/agent_rule.rb activate --table delegation`,
|
|
57
|
+
which pins the entry as absent. An instance with no delegation ruling is
|
|
58
|
+
unaffected and drafts nothing.
|
|
59
|
+
|
|
60
|
+
## [3.91.0] - 2026-10-07
|
|
61
|
+
|
|
62
|
+
### Added — agent: a shadow judge beside the operator (approval delegation, phase 1)
|
|
63
|
+
|
|
64
|
+
At the points the new delegation table covers, a fresh model answers the
|
|
65
|
+
operator's question beside the operator, and the two answers are compared
|
|
66
|
+
later. **In this phase the judge's answer changes nothing**: every point still
|
|
67
|
+
waits for you.
|
|
68
|
+
|
|
69
|
+
- **The delegation table** (`lib/agent/delegation_table.yml`) names two
|
|
70
|
+
points: plan approval in a manual session (`plan_proposed`) and the
|
|
71
|
+
scheduled checkpoint (`cycle_checkpoint`). Every row is `shadow`; the loader
|
|
72
|
+
refuses a table with any other mode or with an `mlr` approver. Like the
|
|
73
|
+
act-route table it is in force only by your terminal ruling, which also pins
|
|
74
|
+
the instruction mode the judge reads; editing that mode takes the table out
|
|
75
|
+
of force until you rule again:
|
|
76
|
+
|
|
77
|
+
ruby .kairos/skillsets/agent/bin/agent_rule.rb activate --table delegation
|
|
78
|
+
|
|
79
|
+
With no ruling, behaviour is as before.
|
|
80
|
+
- **Which points are judged**: only when the act route classifies every step of
|
|
81
|
+
the plan, no step is marked for a person, nothing touches L0, and the goal is
|
|
82
|
+
unchanged since the run started. Rows and precedents are chosen only by
|
|
83
|
+
signals the driver observes from the plan's structure — never by the plan's
|
|
84
|
+
summary wording or its own risk labels.
|
|
85
|
+
- **The judge** is a separate process (`bin/agent_shadow_judge.rb`) started
|
|
86
|
+
once the session has stopped at the point; nothing waits for it. It asks
|
|
87
|
+
`claude-opus-5-5` at effort `xhigh` through `llm_call` (`claude -p`,
|
|
88
|
+
sandboxed, no fallback provider) with the plan, the driver's signals, the
|
|
89
|
+
goal, your instruction mode and up to five of your earlier terminal answers
|
|
90
|
+
at the same kind of stop. Anything but a clear answer from that model counts
|
|
91
|
+
as "hand back to the operator".
|
|
92
|
+
- **Sealed, then shown**: the verdict goes on the chain as a salted commitment
|
|
93
|
+
(`agent_shadow_seal`) and is shown only after your answer at that point has
|
|
94
|
+
committed — in that `agent_step` response and in `agent_status` under
|
|
95
|
+
`shadow`. Before then `agent_status` says only `judging`, `sealed` or
|
|
96
|
+
`failed`. The verdict file sits in the session directory; reading it before
|
|
97
|
+
you answer defeats the comparison.
|
|
98
|
+
- **Counting**: `agent_rule.rb shadow` counts agreement only for answers you
|
|
99
|
+
typed at the terminal whose seal came first, approve and non-approve apart.
|
|
100
|
+
- Each judged point is one Opus call at `xhigh`.
|
|
101
|
+
|
|
102
|
+
### Changed — agent: a run with a failed act is not the scheduled checkpoint
|
|
103
|
+
|
|
104
|
+
An autonomous run now stops as `cycle_checkpoint` only when every act of the
|
|
105
|
+
run succeeded — including an act resumed after a risk pause. A run in which an
|
|
106
|
+
act failed without an act error (a step failed or was denied, the guard failed
|
|
107
|
+
it) stops as `act_failed`, as a failed manual-mode act already did.
|
|
108
|
+
|
|
109
|
+
### Fixed — agent: a torn read of the chain no longer empties the act-route table
|
|
110
|
+
|
|
111
|
+
The table loaders retry a read that overlaps a chain append, as the answer
|
|
112
|
+
reader already did.
|
|
113
|
+
|
|
7
114
|
## [3.90.0] - 2026-10-07
|
|
8
115
|
|
|
9
116
|
### Added — agent: every answer is a ruling on the chain, and an answer typed at a terminal binds
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
#
|
|
4
|
+
# The distiller's process (distillation design v0.2, INV-K1 / INV-K4).
|
|
5
|
+
# Started detached by the driver after an attested, reasoned answer at a
|
|
6
|
+
# delegable point commits, or in the foreground by `agent_rule.rb distill`.
|
|
7
|
+
# Counting, the one-draft lock, the table's status and the inputs are all
|
|
8
|
+
# decided here, off the response path; DistillRun does the run.
|
|
9
|
+
#
|
|
10
|
+
# It loads no agent tool and never opens a session's advance lock. What it
|
|
11
|
+
# reads is what the operator authored and what the driver observed: the
|
|
12
|
+
# attested answers on the chain, the terminal-sourced reasons they name, the
|
|
13
|
+
# point facts they name, and the operator's changes recorded at adoption.
|
|
14
|
+
#
|
|
15
|
+
# The model is reached through llm_call as the shadow judge reaches it:
|
|
16
|
+
# provider claude_code with sandbox_mode, no fallback.
|
|
17
|
+
#
|
|
18
|
+
# argv: [--manual] run whatever the count (the operator asked for it)
|
|
19
|
+
# env: KAIROS_SERVER_LIB lib dir to load kairos_mcp from
|
|
20
|
+
# KAIROS_DATA_DIR the server's data dir (the chain read and written)
|
|
21
|
+
# KAIROS_PROJECT_ROOT the server's working directory (its .kairos/storage)
|
|
22
|
+
|
|
23
|
+
require 'json'
|
|
24
|
+
|
|
25
|
+
manual = ARGV.include?('--manual')
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
Process.setsid unless manual
|
|
29
|
+
rescue StandardError
|
|
30
|
+
# Already a group leader (spawned with pgroup): acceptable.
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
Dir.chdir(ENV['KAIROS_PROJECT_ROOT']) if ENV['KAIROS_PROJECT_ROOT'] && Dir.exist?(ENV['KAIROS_PROJECT_ROOT'])
|
|
35
|
+
$LOAD_PATH.unshift(ENV['KAIROS_SERVER_LIB']) if ENV['KAIROS_SERVER_LIB'] && !ENV['KAIROS_SERVER_LIB'].empty?
|
|
36
|
+
require 'kairos_mcp'
|
|
37
|
+
KairosMcp.data_dir = ENV['KAIROS_DATA_DIR'] if ENV['KAIROS_DATA_DIR'] && !ENV['KAIROS_DATA_DIR'].empty?
|
|
38
|
+
require 'kairos_mcp/kairos_chain/chain'
|
|
39
|
+
|
|
40
|
+
lib = File.expand_path('../lib', __dir__)
|
|
41
|
+
%w[session admission mandate_adapter act_classification answer_ruling delegation point_facts distiller
|
|
42
|
+
distill_run distiller_prompt].each { |f| require File.join(lib, 'agent', f) }
|
|
43
|
+
require 'kairos_mcp/tools/base_tool'
|
|
44
|
+
# agent depends on llm_client; both are installed side by side.
|
|
45
|
+
require File.expand_path('../../llm_client/tools/llm_call', __dir__)
|
|
46
|
+
rescue ScriptError, StandardError => e
|
|
47
|
+
warn JSON.generate({ 'status' => 'failed', 'reason' => "bootstrap: #{e.class}: #{e.message[0, 200]}" })
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
agent = KairosMcp::SkillSets::Agent
|
|
52
|
+
ar = agent::AnswerRuling
|
|
53
|
+
pf = agent::PointFacts
|
|
54
|
+
sessions = agent::Session.storage_path('agent_sessions')
|
|
55
|
+
dir = agent::Session.storage_path('agent_distill')
|
|
56
|
+
|
|
57
|
+
session_dir = lambda do |sid|
|
|
58
|
+
next nil unless sid.to_s.match?(/\A[\w-]+\z/)
|
|
59
|
+
|
|
60
|
+
d = File.join(sessions, sid.to_s)
|
|
61
|
+
File.directory?(d) ? d : nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The reason the operator typed at the terminal, matched by the digest the
|
|
65
|
+
# ruling names. A caller's text is never the operator's reason.
|
|
66
|
+
reason = lambda do |sid, anchor, sha|
|
|
67
|
+
d = session_dir.call(sid)
|
|
68
|
+
next nil unless d
|
|
69
|
+
|
|
70
|
+
ar.texts_at(d, anchor, source: 'terminal').map { |t| t['rationale'] }.find { |r| r && ar.sha256(r) == sha }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The plans the answer covered, as the driver filed them when the answer
|
|
74
|
+
# committed; nil unless the filed line is the one the ruling names. An answer
|
|
75
|
+
# at a point always covers a plan, so an empty window is facts not recorded.
|
|
76
|
+
# Only what the driver observed passes: the cycle, and per step the tool and
|
|
77
|
+
# effect the table gave it, the unclassified count and the step count. The
|
|
78
|
+
# plan's own marks for a person are the agent's declaration and stay behind.
|
|
79
|
+
facts = lambda do |sid, anchor, sha|
|
|
80
|
+
d = session_dir.call(sid)
|
|
81
|
+
next nil unless d
|
|
82
|
+
|
|
83
|
+
entry, line_sha = pf.answer_facts(d, anchor)
|
|
84
|
+
plans = entry && line_sha == sha ? entry['plans'] : nil
|
|
85
|
+
next nil unless plans.is_a?(Array) && !plans.empty?
|
|
86
|
+
next nil unless plans.all? { |p| p.is_a?(Hash) && p['facts'].is_a?(Hash) }
|
|
87
|
+
|
|
88
|
+
plans.map do |p|
|
|
89
|
+
f = p['facts']
|
|
90
|
+
{ 'cycle' => p['cycle'], 'facts' => f.slice('steps', 'unclassified', 'step_count') }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
edits = Dir.glob(File.join(dir, 'adoptions', '*.json')).sort.filter_map do |f|
|
|
95
|
+
e = JSON.parse(File.read(f))
|
|
96
|
+
e.is_a?(Hash) ? e.slice('adopted_block', 'changes') : nil
|
|
97
|
+
rescue StandardError
|
|
98
|
+
nil
|
|
99
|
+
end.sort_by { |e| e['adopted_block'].to_i }
|
|
100
|
+
|
|
101
|
+
llm = lambda do |system:, user:, model:, effort:, provider:|
|
|
102
|
+
out = KairosMcp::SkillSets::LlmClient::Tools::LlmCall.new(nil).call(
|
|
103
|
+
'messages' => [{ 'role' => 'user', 'content' => user }], 'system' => system, 'model' => model,
|
|
104
|
+
'effort' => effort, 'sandbox_mode' => true, 'provider_override' => provider
|
|
105
|
+
)
|
|
106
|
+
payload = JSON.parse(Array(out).first[:text])
|
|
107
|
+
raise "llm_call #{payload.dig('error', 'type')}: #{payload.dig('error', 'message')}" unless payload['status'] == 'ok'
|
|
108
|
+
|
|
109
|
+
payload['response'].merge('provider' => payload['provider'])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
records, records_error = ar.chain_records
|
|
113
|
+
if records_error
|
|
114
|
+
warn JSON.generate({ 'status' => 'failed', 'reason' => "chain unreadable: #{records_error}" })
|
|
115
|
+
exit 1
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
result = agent::DistillRun.run(
|
|
119
|
+
dir: dir, delegation_status: agent::Delegation.load_effective['status'], records: records,
|
|
120
|
+
facts: facts, reason: reason, edits: edits, llm: llm, chain: KairosMcp::KairosChain::Chain.new,
|
|
121
|
+
judge: agent::Delegation::JUDGE, prompts: agent::DistillerPrompt, manual: manual
|
|
122
|
+
)
|
|
123
|
+
(manual ? $stdout : $stderr).puts JSON.generate(result)
|
|
124
|
+
exit(%w[failed].include?(result['status']) ? 1 : 0)
|
|
@@ -2,23 +2,33 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
#
|
|
4
4
|
# The operator's terminal rulings for the agent (design v0.3, INV-A2 / INV-D8):
|
|
5
|
-
# the act-route allow-list, and answers at a stopped
|
|
5
|
+
# the act-route allow-list, the delegation table, and answers at a stopped
|
|
6
|
+
# session.
|
|
6
7
|
#
|
|
7
8
|
# ruby .kairos/skillsets/agent/bin/agent_rule.rb activate # rule the current table into force
|
|
8
9
|
# ruby .kairos/skillsets/agent/bin/agent_rule.rb withdraw # take it out of force
|
|
9
10
|
# ruby .kairos/skillsets/agent/bin/agent_rule.rb status # show what is in force (records nothing)
|
|
10
11
|
# ruby .kairos/skillsets/agent/bin/agent_rule.rb answer SESSION_ID
|
|
11
12
|
# # answer a stopped session; the MCP answer there must then match
|
|
13
|
+
# ruby .kairos/skillsets/agent/bin/agent_rule.rb shadow # count the shadow judge's agreement
|
|
14
|
+
# # with your answers, per version of what
|
|
15
|
+
# # shaped the judge (records nothing)
|
|
16
|
+
# ruby .kairos/skillsets/agent/bin/agent_rule.rb distill # draft judgment precedents now, whatever
|
|
17
|
+
# # the count (calls the distiller model)
|
|
18
|
+
# ruby .kairos/skillsets/agent/bin/agent_rule.rb adopt [STAMP]
|
|
19
|
+
# # adopt a draft (the newest by default) after editing its .edit.md copy
|
|
12
20
|
#
|
|
13
|
-
# Options: --
|
|
21
|
+
# Options: --table NAME activate / withdraw: act_classification (default)
|
|
22
|
+
# or delegation
|
|
23
|
+
# --data-dir DIR the instance data dir whose chain is written
|
|
14
24
|
# (default: the .kairos this script is installed in)
|
|
15
|
-
# --project-dir DIR answer
|
|
16
|
-
# whose .kairos/storage holds its agent sessions
|
|
25
|
+
# --project-dir DIR answer / shadow: the directory the MCP server runs
|
|
26
|
+
# in, whose .kairos/storage holds its agent sessions
|
|
17
27
|
# (default: the directory holding the data dir).
|
|
18
28
|
# Needed when the server runs with --data-dir
|
|
19
29
|
# pointing at another project's .kairos.
|
|
20
30
|
#
|
|
21
|
-
# activate, withdraw and
|
|
31
|
+
# activate, withdraw, answer and adopt read a nonce typed back at /dev/tty and refuse to run
|
|
22
32
|
# without a terminal. That keeps an MCP call from producing a ruling through
|
|
23
33
|
# this path. It does not prove a person typed: any process that can start
|
|
24
34
|
# programs can open a pseudo-terminal, read the nonce and type it back (shown
|
|
@@ -27,11 +37,19 @@
|
|
|
27
37
|
# was at the keyboard (INV-D8 boundary; the constrained party is the agent).
|
|
28
38
|
|
|
29
39
|
require 'json'
|
|
40
|
+
require 'rbconfig'
|
|
30
41
|
|
|
31
|
-
action = ARGV.find { |a| %w[activate withdraw status answer].include?(a) }
|
|
32
|
-
abort 'usage: agent_rule.rb activate|withdraw|status|answer SESSION_ID
|
|
42
|
+
action = ARGV.find { |a| %w[activate withdraw status answer shadow distill adopt].include?(a) }
|
|
43
|
+
abort 'usage: agent_rule.rb activate|withdraw [--table act_classification|delegation]|status|answer SESSION_ID|shadow|' \
|
|
44
|
+
'distill|adopt [STAMP] [--data-dir DIR] [--project-dir DIR]' unless action
|
|
33
45
|
session_id = action == 'answer' ? ARGV[ARGV.index('answer') + 1].to_s : nil
|
|
34
46
|
abort 'usage: agent_rule.rb answer SESSION_ID [--data-dir DIR]' if session_id&.then { |s| s.empty? || s.start_with?('-') }
|
|
47
|
+
stamp = action == 'adopt' ? ARGV[ARGV.index('adopt') + 1] : nil
|
|
48
|
+
stamp = nil if stamp.to_s.start_with?('-')
|
|
49
|
+
|
|
50
|
+
tidx = ARGV.index('--table')
|
|
51
|
+
table = tidx ? ARGV[tidx + 1].to_s : 'act_classification'
|
|
52
|
+
abort "--table must be act_classification or delegation, not #{table.inspect}" unless %w[act_classification delegation].include?(table)
|
|
35
53
|
|
|
36
54
|
idx = ARGV.index('--data-dir')
|
|
37
55
|
data_dir = idx ? File.expand_path(ARGV[idx + 1].to_s) : File.expand_path('../../..', __dir__)
|
|
@@ -50,30 +68,41 @@ lib = File.expand_path('../lib', __dir__)
|
|
|
50
68
|
require File.join(lib, 'agent', 'admission')
|
|
51
69
|
require File.join(lib, 'agent', 'mandate_adapter')
|
|
52
70
|
require File.join(lib, 'agent', 'act_classification')
|
|
71
|
+
require File.join(lib, 'agent', 'answer_ruling')
|
|
72
|
+
require File.join(lib, 'agent', 'delegation')
|
|
73
|
+
require File.join(lib, 'agent', 'shadow_judge')
|
|
74
|
+
require File.join(lib, 'agent', 'point_facts')
|
|
75
|
+
require File.join(lib, 'agent', 'distiller')
|
|
76
|
+
require File.join(lib, 'agent', 'distill_run')
|
|
77
|
+
require File.join(lib, 'agent', 'adoption')
|
|
53
78
|
ac = KairosMcp::SkillSets::Agent::ActClassification
|
|
79
|
+
dl = KairosMcp::SkillSets::Agent::Delegation
|
|
54
80
|
path = ac::BASE_PATH
|
|
55
81
|
|
|
56
82
|
puts "Data dir: #{data_dir}"
|
|
57
|
-
if
|
|
83
|
+
if %w[answer shadow status distill adopt].include?(action)
|
|
58
84
|
require 'kairos_mcp/invocation_context'
|
|
59
85
|
require File.join(lib, 'agent', 'session')
|
|
60
86
|
require File.join(lib, 'agent', 'advance_gate')
|
|
61
|
-
require File.join(lib, 'agent', 'answer_ruling')
|
|
62
87
|
# The server keeps agent sessions under <its working directory>/.kairos/storage
|
|
63
88
|
# (Session.storage_path without Autonomos loaded), and its chain under its
|
|
64
89
|
# data dir. The two can differ; the answer is read from the first and
|
|
65
90
|
# recorded on the second, the chain the server reads.
|
|
66
91
|
pidx = ARGV.index('--project-dir')
|
|
67
|
-
|
|
92
|
+
if pidx && ARGV[pidx + 1].to_s.then { |v| v.empty? || v.start_with?('-') }
|
|
93
|
+
abort "usage: agent_rule.rb #{action == 'answer' ? 'answer SESSION_ID' : action} --project-dir DIR"
|
|
94
|
+
end
|
|
68
95
|
project_dir = pidx ? File.expand_path(ARGV[pidx + 1].to_s) : File.dirname(data_dir)
|
|
69
96
|
abort "no project dir at #{project_dir}" unless File.directory?(project_dir)
|
|
70
97
|
Dir.chdir(project_dir)
|
|
71
|
-
session = KairosMcp::SkillSets::Agent::Session.load(session_id)
|
|
72
|
-
unless session
|
|
73
|
-
abort "no agent session #{session_id} under #{File.join(Dir.pwd, '.kairos', 'storage', 'agent_sessions')} " \
|
|
74
|
-
'(if the MCP server runs in another directory, pass --project-dir)'
|
|
75
|
-
end
|
|
76
98
|
puts "Sessions from: #{project_dir}"
|
|
99
|
+
if action == 'answer'
|
|
100
|
+
session = KairosMcp::SkillSets::Agent::Session.load(session_id)
|
|
101
|
+
unless session
|
|
102
|
+
abort "no agent session #{session_id} under #{File.join(Dir.pwd, '.kairos', 'storage', 'agent_sessions')} " \
|
|
103
|
+
'(if the MCP server runs in another directory, pass --project-dir)'
|
|
104
|
+
end
|
|
105
|
+
end
|
|
77
106
|
end
|
|
78
107
|
if action == 'status'
|
|
79
108
|
eff = ac.load_effective(path: path)
|
|
@@ -82,6 +111,78 @@ if action == 'status'
|
|
|
82
111
|
puts " ruled sha256: #{eff['ruled_sha256'] || '(none)'}"
|
|
83
112
|
puts " detail: #{eff['detail']}" if eff['detail']
|
|
84
113
|
puts " tools in force: #{eff['tools'].keys.sort.join(', ')}" unless eff['tools'].empty?
|
|
114
|
+
deff = dl.load_effective
|
|
115
|
+
puts "Table #{dl::TABLE_ID}: #{deff['status']}"
|
|
116
|
+
puts " file sha256: #{deff['sha256']}"
|
|
117
|
+
puts " detail: #{deff['detail']}" if deff['detail']
|
|
118
|
+
deff['rows'].each { |r| puts " row #{r['id']} at #{r['point']} (#{r['mode']})" }
|
|
119
|
+
deff['reads'].each do |p|
|
|
120
|
+
puts(p['absent'] ? " judge reads #{p['name']}: #{p['path']} (absent: nothing adopted yet)" :
|
|
121
|
+
" judge reads #{p['name']}: #{p['path']} (sha256 #{p['sha256']})")
|
|
122
|
+
end
|
|
123
|
+
# Drafting (distillation design v0.2 INV-K4): shown here, never in an advance.
|
|
124
|
+
distill_dir = KairosMcp::SkillSets::Agent::Session.storage_path('agent_distill')
|
|
125
|
+
drun = KairosMcp::SkillSets::Agent::DistillRun
|
|
126
|
+
st = drun.state(distill_dir)
|
|
127
|
+
records, rerr = KairosMcp::SkillSets::Agent::AnswerRuling.chain_records
|
|
128
|
+
counted = rerr ? "unknown (#{rerr})" :
|
|
129
|
+
KairosMcp::SkillSets::Agent::Distiller.count_new(records, high_water: st['high_water'].to_i)
|
|
130
|
+
puts 'Judgment precedents drafting:'
|
|
131
|
+
puts " reasoned answers since the last draft: #{counted} (a draft starts at #{KairosMcp::SkillSets::Agent::Distiller::THRESHOLD}; " \
|
|
132
|
+
"only while the delegation table is in force)"
|
|
133
|
+
last = st['last'] || {}
|
|
134
|
+
puts " last run: #{last.empty? ? '(none)' : last.map { |k, v| "#{k} #{v}" }.join(', ')}"
|
|
135
|
+
if last['status'] == 'running'
|
|
136
|
+
# A run holds the lock for its whole life; a free lock means it died.
|
|
137
|
+
# Read the state again while holding the lock: a run that finished in
|
|
138
|
+
# between released it, and is not dead.
|
|
139
|
+
died = File.open(File.join(distill_dir, drun::LOCK_FILE), 'a') do |f|
|
|
140
|
+
f.flock(File::LOCK_EX | File::LOCK_NB) && drun.state(distill_dir).dig('last', 'status') == 'running'
|
|
141
|
+
end
|
|
142
|
+
puts ' (no process holds the drafting lock: that run died before it finished)' if died
|
|
143
|
+
end
|
|
144
|
+
drafts = drun.drafts(distill_dir)
|
|
145
|
+
puts " drafts: #{drafts.empty? ? '(none)' : drafts.first(3).map { |d| d['stamp'] }.join(', ')}"
|
|
146
|
+
puts " to adopt the newest: edit #{drafts.first['edit_path']}, then run agent_rule.rb adopt" unless drafts.empty?
|
|
147
|
+
exit 0
|
|
148
|
+
end
|
|
149
|
+
if action == 'distill'
|
|
150
|
+
# The worker in the foreground, run by the operator: the same code path a
|
|
151
|
+
# count starts, without the count.
|
|
152
|
+
env = { 'KAIROS_PROJECT_ROOT' => Dir.pwd, 'KAIROS_DATA_DIR' => data_dir }
|
|
153
|
+
env['KAIROS_SERVER_LIB'] = ENV['KAIROS_SERVER_LIB'] if ENV['KAIROS_SERVER_LIB']
|
|
154
|
+
puts 'Drafting (this calls the distiller model and may take several minutes)...'
|
|
155
|
+
ok = system(env, RbConfig.ruby, File.expand_path('agent_distill.rb', __dir__), '--manual')
|
|
156
|
+
exit(ok ? 0 : 1)
|
|
157
|
+
end
|
|
158
|
+
if action == 'shadow'
|
|
159
|
+
ar = KairosMcp::SkillSets::Agent::AnswerRuling
|
|
160
|
+
sj = KairosMcp::SkillSets::Agent::ShadowJudge
|
|
161
|
+
records, error = ar.chain_records
|
|
162
|
+
abort "The chain could not be read (#{error}); nothing to count." if error
|
|
163
|
+
sessions = File.expand_path(File.join('.kairos', 'storage', 'agent_sessions'))
|
|
164
|
+
verdict_dir = lambda do |sid, anchor|
|
|
165
|
+
sid.to_s.match?(/\A[\w-]+\z/) ? sj.anchor_dir(File.join(sessions, sid.to_s), anchor) : File::NULL
|
|
166
|
+
end
|
|
167
|
+
# Counted apart for each version of what shaped the judge (distillation
|
|
168
|
+
# design v0.2 INV-K5): a new adoption, a mode edit or a new judge prompt
|
|
169
|
+
# starts a new count; earlier counts are shown, not merged.
|
|
170
|
+
by = sj.agreement_by_version(records, verdict_dir: verdict_dir)
|
|
171
|
+
total = by['versions'].sum { |v| v['summary']['pairs'] }
|
|
172
|
+
puts "Pairs that count as evidence (your terminal answer, sealed before it): #{total}, " \
|
|
173
|
+
"in #{by['versions'].size} version(s), newest first"
|
|
174
|
+
by['versions'].each_with_index do |v, i|
|
|
175
|
+
sum = v['summary']
|
|
176
|
+
key = v['version']
|
|
177
|
+
yes = sum['operator_approved']
|
|
178
|
+
no = sum['operator_did_not_approve']
|
|
179
|
+
puts "Version #{i + 1} (seals ##{v['first_seal']}..##{v['last_seal']}): judge prompt #{key['judge_prompt'].to_s[0, 12].then { |x| x.empty? ? '(before 3.92.0)' : x }}, " \
|
|
180
|
+
"reads #{key['reads'].map { |r| r.sub(/:(\h{12})\h+\z/, ':\1') }.join(' ')}"
|
|
181
|
+
puts " you approved: #{yes['n']}; the judge also approved #{yes['judge_agreed']}"
|
|
182
|
+
puts " you did not approve: #{no['n']}; the judge gave the same answer #{no['judge_agreed']}, approved #{no['judge_approved']}"
|
|
183
|
+
puts " the judge approved #{sum['judge_approved']} of #{sum['pairs']}"
|
|
184
|
+
end
|
|
185
|
+
puts "Not counted: #{by['not_counted'].empty? ? '(none)' : by['not_counted'].map { |k, v| "#{k} #{v}" }.join(', ')}"
|
|
85
186
|
exit 0
|
|
86
187
|
end
|
|
87
188
|
|
|
@@ -92,11 +193,19 @@ rescue StandardError => e
|
|
|
92
193
|
end
|
|
93
194
|
|
|
94
195
|
begin
|
|
95
|
-
result = if action == '
|
|
196
|
+
result = if action == 'adopt'
|
|
197
|
+
KairosMcp::SkillSets::Agent::Adoption.interactive_adopt(
|
|
198
|
+
dir: KairosMcp::SkillSets::Agent::Session.storage_path('agent_distill'),
|
|
199
|
+
sessions_dir: KairosMcp::SkillSets::Agent::Session.storage_path('agent_sessions'),
|
|
200
|
+
stamp: stamp, tty_in: tty, tty_out: tty
|
|
201
|
+
)
|
|
202
|
+
elsif action == 'answer'
|
|
96
203
|
KairosMcp::SkillSets::Agent::AnswerRuling.interactive_answer(
|
|
97
204
|
session: session, gate: KairosMcp::SkillSets::Agent::AdvanceGate.new(session.guard_dir),
|
|
98
205
|
tty_in: tty, tty_out: tty, reload: -> { KairosMcp::SkillSets::Agent::Session.load(session_id) }
|
|
99
206
|
)
|
|
207
|
+
elsif table == 'delegation'
|
|
208
|
+
dl.interactive_rule(action: action, tty_in: tty, tty_out: tty)
|
|
100
209
|
else
|
|
101
210
|
ac.interactive_rule(action: action, tty_in: tty, tty_out: tty, path: path)
|
|
102
211
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
#
|
|
4
|
+
# The shadow judge's process (design v0.3, INV-D6 / INV-D7). Started detached
|
|
5
|
+
# by the driver after an advance commits, once per anchor, with the packet the
|
|
6
|
+
# driver wrote. It reads the files the packet pins, asks a fresh model the
|
|
7
|
+
# operator's question, writes the verdict beside the packet and appends a
|
|
8
|
+
# salted commitment to it on the chain.
|
|
9
|
+
#
|
|
10
|
+
# It loads no agent tool and never opens the session's advance lock: nothing
|
|
11
|
+
# waits for it, and nothing it does can reach the operator's answer.
|
|
12
|
+
#
|
|
13
|
+
# The model is reached through llm_call (design v0.3 §4), loaded here as a
|
|
14
|
+
# tool object rather than through a registry: provider claude_code (claude -p)
|
|
15
|
+
# with sandbox_mode, the requested model and effort. llm_call falls back to
|
|
16
|
+
# another provider only from a provider other than claude_code, so the judge
|
|
17
|
+
# has none. Its timeout is llm_client's configured one (600 s as shipped).
|
|
18
|
+
#
|
|
19
|
+
# argv: <packet_path>
|
|
20
|
+
# env: KAIROS_SERVER_LIB lib dir to load kairos_mcp from
|
|
21
|
+
# KAIROS_DATA_DIR the server's data dir (the chain written)
|
|
22
|
+
# KAIROS_PROJECT_ROOT the server's working directory
|
|
23
|
+
|
|
24
|
+
require 'json'
|
|
25
|
+
|
|
26
|
+
packet_path = ARGV[0] or abort 'usage: agent_shadow_judge.rb <packet_path>'
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
Process.setsid
|
|
30
|
+
rescue StandardError
|
|
31
|
+
# Already a group leader (spawned with pgroup): acceptable.
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
Dir.chdir(ENV['KAIROS_PROJECT_ROOT']) if ENV['KAIROS_PROJECT_ROOT'] && Dir.exist?(ENV['KAIROS_PROJECT_ROOT'])
|
|
36
|
+
$LOAD_PATH.unshift(ENV['KAIROS_SERVER_LIB']) if ENV['KAIROS_SERVER_LIB']
|
|
37
|
+
require 'kairos_mcp'
|
|
38
|
+
KairosMcp.data_dir = ENV['KAIROS_DATA_DIR'] if ENV['KAIROS_DATA_DIR'] && !ENV['KAIROS_DATA_DIR'].empty?
|
|
39
|
+
require 'kairos_mcp/kairos_chain/chain'
|
|
40
|
+
|
|
41
|
+
lib = File.expand_path('../lib', __dir__)
|
|
42
|
+
require File.join(lib, 'agent', 'answer_ruling')
|
|
43
|
+
require File.join(lib, 'agent', 'delegation')
|
|
44
|
+
require File.join(lib, 'agent', 'shadow_judge')
|
|
45
|
+
require 'kairos_mcp/tools/base_tool'
|
|
46
|
+
# agent depends on llm_client; both are installed side by side.
|
|
47
|
+
require File.expand_path('../../llm_client/tools/llm_call', __dir__)
|
|
48
|
+
rescue ScriptError, StandardError => e
|
|
49
|
+
dir = File.dirname(packet_path)
|
|
50
|
+
File.write(File.join(dir, 'error.json'),
|
|
51
|
+
JSON.generate({ 'sealed' => false, 'error' => "bootstrap: #{e.class}: #{e.message[0, 200]}" }))
|
|
52
|
+
exit 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
llm = lambda do |system:, user:, model:, effort:, provider:|
|
|
56
|
+
out = KairosMcp::SkillSets::LlmClient::Tools::LlmCall.new(nil).call(
|
|
57
|
+
'messages' => [{ 'role' => 'user', 'content' => user }], 'system' => system, 'model' => model,
|
|
58
|
+
'effort' => effort, 'sandbox_mode' => true, 'provider_override' => provider
|
|
59
|
+
)
|
|
60
|
+
payload = JSON.parse(Array(out).first[:text])
|
|
61
|
+
raise "llm_call #{payload.dig('error', 'type')}: #{payload.dig('error', 'message')}" unless payload['status'] == 'ok'
|
|
62
|
+
|
|
63
|
+
payload['response'].merge('provider' => payload['provider'])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
result = KairosMcp::SkillSets::Agent::ShadowJudge.judge_and_seal(
|
|
67
|
+
packet_path, llm: llm, chain: KairosMcp::KairosChain::Chain.new
|
|
68
|
+
)
|
|
69
|
+
warn JSON.generate(result)
|
|
70
|
+
exit(result['sealed'] ? 0 : 1)
|
|
@@ -92,16 +92,30 @@ module KairosMcp
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# [rulings, nil] from the instance chain, or [[], reason] when the
|
|
95
|
-
# chain cannot be read (which empties the table).
|
|
95
|
+
# chain cannot be read (which empties the table). The ledger is
|
|
96
|
+
# rewritten in place on every append, and the shadow judge appends
|
|
97
|
+
# from its own process at any moment, so a read is retried before a
|
|
98
|
+
# torn one empties the table (as AnswerRuling.chain_records).
|
|
99
|
+
READ_ATTEMPTS = 3
|
|
100
|
+
|
|
96
101
|
def chain_rulings
|
|
97
102
|
return ActClassification.rulings_source.call if ActClassification.rulings_source
|
|
98
103
|
|
|
99
104
|
require 'kairos_mcp/kairos_chain/chain'
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
reason = nil
|
|
106
|
+
READ_ATTEMPTS.times do |i|
|
|
107
|
+
sleep(0.05 * i) if i.positive?
|
|
108
|
+
begin
|
|
109
|
+
chain = ::KairosMcp::KairosChain::Chain.new
|
|
110
|
+
state = chain.load_state
|
|
111
|
+
return [rulings_from_blocks(chain.chain), nil] if %i[readable absent].include?(state)
|
|
112
|
+
|
|
113
|
+
reason = "chain #{state}"
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
reason = "chain unreadable: #{e.class}: #{e.message[0, 120]}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
[[], reason]
|
|
105
119
|
rescue StandardError, ScriptError => e
|
|
106
120
|
[[], "chain unreadable: #{e.class}: #{e.message[0, 120]}"]
|
|
107
121
|
end
|