kairos-chain 3.49.0 → 3.51.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 +92 -1
- data/bin/kairos-chain +30 -7
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/agent/bin/agent_step_worker.rb +183 -0
- data/templates/skillsets/agent/lib/agent/advance_gate.rb +357 -0
- data/templates/skillsets/agent/lib/agent/session.rb +12 -3
- data/templates/skillsets/agent/lib/agent/step_delegation.rb +344 -0
- data/templates/skillsets/agent/lib/agent.rb +2 -0
- data/templates/skillsets/agent/skillset.json +2 -1
- data/templates/skillsets/agent/test/test_agent_interruption_resilience.rb +729 -0
- data/templates/skillsets/agent/tools/agent_status.rb +42 -3
- data/templates/skillsets/agent/tools/agent_step.rb +299 -18
- data/templates/skillsets/agent/tools/agent_stop.rb +52 -14
- data/templates/skillsets/agent/tools/agent_wait.rb +191 -0
- data/templates/skillsets/chain_distillation/config/chain_distillation.yml +13 -0
- data/templates/skillsets/chain_distillation/lib/chain_distillation/canon.rb +44 -0
- data/templates/skillsets/chain_distillation/lib/chain_distillation/certificate.rb +308 -0
- data/templates/skillsets/chain_distillation/lib/chain_distillation/distiller.rb +305 -0
- data/templates/skillsets/chain_distillation/lib/chain_distillation/recorder.rb +144 -0
- data/templates/skillsets/chain_distillation/skillset.json +18 -0
- data/templates/skillsets/chain_distillation/test/test_chain_distillation.rb +464 -0
- data/templates/skillsets/chain_distillation/tools/cd_distill.rb +67 -0
- data/templates/skillsets/chain_distillation/tools/cd_revoke.rb +57 -0
- data/templates/skillsets/chain_distillation/tools/cd_verify.rb +56 -0
- data/templates/skillsets/confidentiality_guard/config/profile.example.yml +9 -0
- data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/policy.rb +8 -0
- data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/recorder.rb +4 -1
- data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/regime.rb +4 -1
- data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/surfaces.rb +21 -0
- data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/verdict.rb +14 -0
- data/templates/skillsets/confidentiality_guard/test/test_cg_guard.rb +47 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fea78afc847b26db63d7355188444750fdb3cbbf84494cc489e63402bea3b630
|
|
4
|
+
data.tar.gz: b638968a01320bf5196d7f2b383b5c6dfa118a9432dc6feadf4ef04cbd4ee6b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0be2e69efbd5eda0c8c839c627389def7dd68db5cda29a904072e762677238459ef6a2fad8373d612715f1eeeb928579a7a318bc97ffdd5fb6c38eb54991c522
|
|
7
|
+
data.tar.gz: 74fd8b7ba83ef25552130dbd7b86047ce96fda674b1d151b247813597adef09c60ec192c524507ad054d3af3c63154b497de71d912891f84cad8637e79bd1bdf
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,97 @@ 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.51.0] - 2026-07-23
|
|
8
|
+
|
|
9
|
+
### Agent SkillSet — interruption resilience A-1 + A-2
|
|
10
|
+
|
|
11
|
+
Addresses the failure mode where the orchestrator's API stalling leaves an
|
|
12
|
+
auto-mode agent loop waiting on input: the running MCP server is intact, but
|
|
13
|
+
the driver stops, and auto-permission is orthogonal to driver liveness.
|
|
14
|
+
Design v0.3.1 FROZEN (design-by-invariant, multi-LLM review 3R converged).
|
|
15
|
+
|
|
16
|
+
- **A-1 — AdvanceGate** (`lib/agent/advance_gate.rb`): every state-advancing
|
|
17
|
+
`agent_step` call passes through a per-session serialized, atomic, anchored
|
|
18
|
+
gate. INV-A2 (serialized atomic advance via `advance.lock` + tmp-rename
|
|
19
|
+
commit), INV-A3 (anchored at-most-once with a `seq:state:cycle` anchor,
|
|
20
|
+
committed outcomes replay on retry, side-effect intent bracket with
|
|
21
|
+
no-silent-drop), INV-A4 (`next_move` derivable from persisted state),
|
|
22
|
+
INV-A5 (adjudication as a gated judgment). `adjudicate` action added.
|
|
23
|
+
Impl review 4R converged, 49 probes.
|
|
24
|
+
- **A-2 — delegated step executor** (`lib/agent/step_delegation.rb`,
|
|
25
|
+
`bin/agent_step_worker.rb`): INV-A1 driver independence. `agent_step` with
|
|
26
|
+
`execution: "delegated"` returns a resumable handle while a detached
|
|
27
|
+
(setsid) worker re-enters the SAME gated `agent_step` under server-side
|
|
28
|
+
ownership — delegate → wait → collect, transplanted from the review
|
|
29
|
+
SkillSet. All correctness inherited from the A-1 gate; the delegation layer
|
|
30
|
+
is coordination/observability only. New `agent_wait` tool; `agent_status`
|
|
31
|
+
surfaces the handle. Impl review 6R converged, 81 probes total.
|
|
32
|
+
- Attended real-worker run with a live LLM-bearing step validated
|
|
33
|
+
(2026-07-23, isolated harness): the detached worker bootstrapped the full
|
|
34
|
+
ToolRegistry and ran ORIENT+DECIDE via the claude CLI; liveness, collect,
|
|
35
|
+
and collector-owned teardown all held.
|
|
36
|
+
- Native body path unchanged (selectable-off). All prior agent-suite tests
|
|
37
|
+
remain at the main baseline.
|
|
38
|
+
|
|
39
|
+
## [3.50.0] - 2026-07-23
|
|
40
|
+
|
|
41
|
+
### Chain Distillation SkillSet — slice 1 (CD-1..CD-6)
|
|
42
|
+
|
|
43
|
+
New `chain_distillation` SkillSet implementing design v0.5 (review-converged
|
|
44
|
+
5/6 APPROVE + persona unanimity): provenance-certified distillation of the
|
|
45
|
+
instance chain into distributable SkillSets.
|
|
46
|
+
|
|
47
|
+
- `cd_distill`: closed-world integer designation (genesis excluded,
|
|
48
|
+
decline-not-coerce), guard-judged distillate and certificate crossings
|
|
49
|
+
(separate, in order, single-encoded presentation), CD-6 constitutive
|
|
50
|
+
record before release (salted sdp-1-style commitments over the distillate
|
|
51
|
+
and the claim core excluding the record citation), content-independent
|
|
52
|
+
pre-assigned certificate identity, attestation-carrier seam
|
|
53
|
+
(`proof_id` = certificate identity), designation-overlap re-issuance
|
|
54
|
+
citation against then-revoked predecessors.
|
|
55
|
+
- `cd_verify`: full-citation positive grounding (claim-core commitment
|
|
56
|
+
opening, identity, designation digest, guard policy sha, distillate
|
|
57
|
+
commitment, predecessors, citation block hash) plus chain-identity /
|
|
58
|
+
head-binding checks; pinned checkability-status table with
|
|
59
|
+
both-direction mislabeling fatal; closed claim-core vocabulary at every
|
|
60
|
+
depth; revocation folds into validity.
|
|
61
|
+
- `cd_revoke`: identity-keyed chain-wide revocation, closed reason
|
|
62
|
+
vocabulary (`superseded|defective|withdrawn|other`).
|
|
63
|
+
- Requires an active confidentiality-guard regime (CD-1: decline, never
|
|
64
|
+
degrade). 60 design-constraint tests incl. production-wiring and
|
|
65
|
+
real-Chain regressions (KAIROS_DATA_DIR-isolated).
|
|
66
|
+
|
|
67
|
+
### Confidentiality Guard — slice-2 first increment (distillation crossing)
|
|
68
|
+
|
|
69
|
+
- New `:distillation_outward` crossing class (`cd_release_distillate`,
|
|
70
|
+
`cd_release_certificate`): conjunctive per-destination verdict
|
|
71
|
+
(closed-world `distillation_crossings` profile designation AND content
|
|
72
|
+
classes), outward verdicts recorded pass or deny (CG-4); the general
|
|
73
|
+
outward class stays denied wholesale.
|
|
74
|
+
- Certificate-crossing verdict records cite the certificate identity
|
|
75
|
+
(identifier-only, CG-4-compatible).
|
|
76
|
+
- Template-layout require-path parity fix in `regime.rb` / `recorder.rb`.
|
|
77
|
+
- Guard test suite: 75 design-constraint + regression tests.
|
|
78
|
+
|
|
79
|
+
## [3.49.1] - 2026-07-22
|
|
80
|
+
|
|
81
|
+
### CLI fix — `skillset upgrade` messaging for non-core new SkillSets
|
|
82
|
+
|
|
83
|
+
`kairos-chain skillset upgrade --apply` always runs with `core_only: true`
|
|
84
|
+
(the plain CLI form has no way to pass explicit `names`), so a new,
|
|
85
|
+
non-core SkillSet (e.g. `confidentiality_guard`, `daemon_runtime`,
|
|
86
|
+
`kairos_hook_projector`) was never installed by `--apply`, regardless of
|
|
87
|
+
how many times it was run — but the check-mode display mislabeled these as
|
|
88
|
+
"files changed" and told the user to run `--apply`, which silently did
|
|
89
|
+
nothing for them.
|
|
90
|
+
|
|
91
|
+
- Detected upgrades are now split into "applicable" (existing-installed
|
|
92
|
+
diffs + core new SkillSets, which `--apply` does handle) and "manual"
|
|
93
|
+
(non-core new SkillSets, which require explicit
|
|
94
|
+
`kairos-chain skillset install <name>`). Both the check-mode listing and
|
|
95
|
+
the post-`--apply` summary report each group accurately.
|
|
96
|
+
- Added the missing `upgrade` line to `kairos-chain skillset -h`.
|
|
97
|
+
|
|
7
98
|
## [3.49.0] - 2026-07-22
|
|
8
99
|
|
|
9
100
|
### ZK aggregate reproducibility spike (AUD-L4, Phases 1+2) — KairosChain's first genuine zero-knowledge proof
|
|
@@ -65,7 +156,7 @@ resource-scheme reads are denied wholesale under the CG-1 coverage clause.
|
|
|
65
156
|
records carry the designation id, not the raw path. `canon.rb`: shared
|
|
66
157
|
canonical serialization faithful to false/nil and mixed string/symbol keys.
|
|
67
158
|
- `tools/cg_status.rb`: read-only regime-state tool (CG-1).
|
|
68
|
-
- 67 design-constraint + regression tests (`test/test_cg_guard.rb`).
|
|
159
|
+
- 67 design-constraint + regression tests (`test/test_cg_guard.rb`); 75 as of the chain_distillation guard slice-2 first increment (distillation crossing).
|
|
69
160
|
|
|
70
161
|
Core (minimal, guard-enabling): `tool_registry.rb` adds `FailClosedError`
|
|
71
162
|
(re-raised by the loader, never swallowed), per-skillset isolated loading,
|
data/bin/kairos-chain
CHANGED
|
@@ -134,6 +134,12 @@ when 'skillset'
|
|
|
134
134
|
when 'upgrade'
|
|
135
135
|
apply = ARGV.delete('--apply')
|
|
136
136
|
upgrades = manager.upgrade_check
|
|
137
|
+
# --apply always runs with core_only: true (no way to pass names from this
|
|
138
|
+
# CLI form), so a new, non-core SkillSet is NEVER installed by --apply,
|
|
139
|
+
# no matter how many times it is run. Split the two groups so the
|
|
140
|
+
# displayed guidance matches what --apply will actually do.
|
|
141
|
+
manual = upgrades.select { |u| u[:new_skillset] && !KairosMcp::SkillSetManager::CORE_SKILLSETS.include?(u[:name]) }
|
|
142
|
+
applicable = upgrades - manual
|
|
137
143
|
|
|
138
144
|
if upgrades.empty?
|
|
139
145
|
puts "All SkillSets are up to date."
|
|
@@ -144,6 +150,12 @@ when 'skillset'
|
|
|
144
150
|
end
|
|
145
151
|
puts "Done. #{results.size} SkillSet(s) upgraded."
|
|
146
152
|
|
|
153
|
+
if manual.any?
|
|
154
|
+
puts ""
|
|
155
|
+
puts "Not installed (optional, requires explicit approval):"
|
|
156
|
+
manual.each { |u| puts " kairos-chain skillset install #{u[:name]}" }
|
|
157
|
+
end
|
|
158
|
+
|
|
147
159
|
# Re-project plugin artifacts if any SkillSets changed
|
|
148
160
|
if results.any?
|
|
149
161
|
begin
|
|
@@ -162,15 +174,25 @@ when 'skillset'
|
|
|
162
174
|
end
|
|
163
175
|
end
|
|
164
176
|
else
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
if applicable.any?
|
|
178
|
+
puts "SkillSet upgrades available:"
|
|
179
|
+
puts ""
|
|
180
|
+
applicable.each do |u|
|
|
181
|
+
label = u[:version_bump] ? "v#{u[:installed_version]} -> v#{u[:available_version]}" : "files changed"
|
|
182
|
+
puts " #{u[:name]}: #{label}"
|
|
183
|
+
u[:changed_files].each { |f| puts " Modified: #{f}" }
|
|
184
|
+
puts ""
|
|
185
|
+
end
|
|
186
|
+
puts "Run 'kairos-chain skillset upgrade --apply' to apply updates."
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if manual.any?
|
|
190
|
+
puts "" if applicable.any?
|
|
191
|
+
puts "Optional SkillSets available (not auto-installed; require explicit approval):"
|
|
192
|
+
manual.each { |u| puts " #{u[:name]} v#{u[:available_version]} — #{u[:description]}" }
|
|
171
193
|
puts ""
|
|
194
|
+
puts "Install with: kairos-chain skillset install <name>"
|
|
172
195
|
end
|
|
173
|
-
puts "Run 'kairos-chain skillset upgrade --apply' to apply updates."
|
|
174
196
|
end
|
|
175
197
|
|
|
176
198
|
when 'remove'
|
|
@@ -258,6 +280,7 @@ when 'skillset'
|
|
|
258
280
|
puts " install <path> [--layer L0|L1|L2] Install a SkillSet from path"
|
|
259
281
|
puts " enable <name> Enable a SkillSet"
|
|
260
282
|
puts " disable <name> Disable a SkillSet"
|
|
283
|
+
puts " upgrade [--apply] Check for/apply SkillSet updates from gem templates"
|
|
261
284
|
puts " remove <name> Remove a SkillSet"
|
|
262
285
|
puts " info <name> Show SkillSet details"
|
|
263
286
|
puts " package <name> Package a knowledge-only SkillSet as JSON archive"
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
#
|
|
4
|
+
# Interruption resilience Slice A-2 — detached agent step worker (INV-A1).
|
|
5
|
+
#
|
|
6
|
+
# Spawned by StepDelegation#spawn_worker after agent_step was called with
|
|
7
|
+
# execution: "delegated". This worker is deliberately thin: it bootstraps a
|
|
8
|
+
# ToolRegistry (the same construction the MCP server uses) and re-enters the
|
|
9
|
+
# SAME gated agent_step path with the recorded arguments (the delegation-start
|
|
10
|
+
# anchor is already injected into those arguments). Every correctness property
|
|
11
|
+
# — per-session serialization, anchored at-most-once, side-effect intent
|
|
12
|
+
# bracket — is enforced by the AdvanceGate inside that call (Slice A-1), not by
|
|
13
|
+
# this script. If this worker dies, the driver re-issues the recorded call
|
|
14
|
+
# safely; if it double-runs, the gate serializes and replays.
|
|
15
|
+
#
|
|
16
|
+
# argv: <session_id> <session_dir>
|
|
17
|
+
# env: KAIROS_PROJECT_ROOT (chdir target)
|
|
18
|
+
# KAIROS_SERVER_LIB (lib dir to load kairos_mcp from)
|
|
19
|
+
# KAIROS_DATA_DIR (the server's effective data dir; makes the
|
|
20
|
+
# worker resolve the SAME .kairos)
|
|
21
|
+
#
|
|
22
|
+
# Exit codes: 0 success; 1 exception; 125 setsid failed; 130 signal.
|
|
23
|
+
#
|
|
24
|
+
# NB: bootstrap failures (LoadError/ScriptError from require) are caught too,
|
|
25
|
+
# so the driver always sees a result rather than a silently hung handle.
|
|
26
|
+
|
|
27
|
+
require 'json'
|
|
28
|
+
require 'time'
|
|
29
|
+
require 'fileutils'
|
|
30
|
+
|
|
31
|
+
session_id = ARGV[0] or abort 'usage: agent_step_worker.rb <session_id> <session_dir>'
|
|
32
|
+
session_dir = ARGV[1] or abort 'usage: agent_step_worker.rb <session_id> <session_dir>'
|
|
33
|
+
|
|
34
|
+
# Read the handle identity from the raw file first, so even a bootstrap
|
|
35
|
+
# failure can tag its error result with the delegation it belongs to.
|
|
36
|
+
def read_handle_identity(session_dir)
|
|
37
|
+
raw = JSON.parse(File.read(File.join(session_dir, 'delegation.json')))
|
|
38
|
+
{ 'issue_anchor' => raw['issue_anchor'], 'action_key' => raw['action_key'],
|
|
39
|
+
'step_token' => raw['step_token'] }
|
|
40
|
+
rescue StandardError
|
|
41
|
+
{}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def write_raw_result(session_dir, identity, outcome)
|
|
45
|
+
payload = identity.merge('outcome' => outcome)
|
|
46
|
+
tmp = File.join(session_dir, "delegation_result.json.tmp.#{Process.pid}")
|
|
47
|
+
File.write(tmp, JSON.generate(payload))
|
|
48
|
+
File.rename(tmp, File.join(session_dir, 'delegation_result.json'))
|
|
49
|
+
rescue StandardError
|
|
50
|
+
# nothing more we can do
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
boot_identity = read_handle_identity(session_dir)
|
|
54
|
+
|
|
55
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
56
|
+
begin
|
|
57
|
+
require 'agent/step_delegation'
|
|
58
|
+
rescue ScriptError, StandardError => e
|
|
59
|
+
# The delegation lib is co-located with this script; if even it cannot load
|
|
60
|
+
# we tag a raw result with the handle identity so status can surface it as
|
|
61
|
+
# 'ready' (an error outcome) rather than the driver waiting out the grace.
|
|
62
|
+
write_raw_result(session_dir, boot_identity,
|
|
63
|
+
{ 'status' => 'error',
|
|
64
|
+
'error' => "worker bootstrap failed: #{e.class}: #{e.message}" })
|
|
65
|
+
exit 1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
delegation = KairosMcp::SkillSets::Agent::StepDelegation.new(session_dir)
|
|
69
|
+
my_token = boot_identity['step_token']
|
|
70
|
+
|
|
71
|
+
shutdown = { requested: false }
|
|
72
|
+
%w[TERM INT HUP].each do |sig|
|
|
73
|
+
Signal.trap(sig) { shutdown[:requested] = true }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
begin
|
|
77
|
+
Process.setsid
|
|
78
|
+
rescue Errno::EPERM
|
|
79
|
+
# Already a session leader — acceptable; continue.
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
delegation.write_result({ 'status' => 'error',
|
|
82
|
+
'error' => "worker setsid failed: #{e.message}" },
|
|
83
|
+
identity: boot_identity)
|
|
84
|
+
exit 125
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
heartbeat_thread = Thread.new do
|
|
88
|
+
loop do
|
|
89
|
+
begin
|
|
90
|
+
delegation.touch_heartbeat(my_token)
|
|
91
|
+
rescue StandardError
|
|
92
|
+
# A transient touch failure must not kill the heartbeat thread and
|
|
93
|
+
# make a live worker look crashed; retry on the next tick.
|
|
94
|
+
end
|
|
95
|
+
sleep KairosMcp::SkillSets::Agent::StepDelegation::HEARTBEAT_INTERVAL_SECONDS
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Self-timeout watchdog: a hung gated call would otherwise hold the advance
|
|
100
|
+
# lock forever. Exiting the process releases its flock; the driver then sees
|
|
101
|
+
# 'crashed' and re-issues safely. We deliberately write NO result here so the
|
|
102
|
+
# collector takes the crash path — if the gated advance had already committed,
|
|
103
|
+
# crashed_response recovers its outcome from the gate log; an error result
|
|
104
|
+
# would instead mask that committed advance.
|
|
105
|
+
timeout_s = KairosMcp::SkillSets::Agent::StepDelegation.worker_self_timeout_seconds
|
|
106
|
+
watchdog = Thread.new do
|
|
107
|
+
sleep timeout_s
|
|
108
|
+
exit!(124)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
begin
|
|
112
|
+
Dir.chdir(ENV['KAIROS_PROJECT_ROOT']) if ENV['KAIROS_PROJECT_ROOT'] &&
|
|
113
|
+
Dir.exist?(ENV['KAIROS_PROJECT_ROOT'])
|
|
114
|
+
$LOAD_PATH.unshift(ENV['KAIROS_SERVER_LIB']) if ENV['KAIROS_SERVER_LIB']
|
|
115
|
+
# KAIROS_DATA_DIR was set in the worker env by spawn_worker so ToolRegistry
|
|
116
|
+
# / Session resolve the server's effective .kairos even under --data-dir.
|
|
117
|
+
require 'kairos_mcp/tool_registry'
|
|
118
|
+
|
|
119
|
+
pending = delegation.pending
|
|
120
|
+
raise "no pending delegation in #{session_dir}" unless pending
|
|
121
|
+
|
|
122
|
+
# Supersession guard: run ONLY if the live handle is still ours. If a fresh
|
|
123
|
+
# open_handle replaced it while we were starting up (e.g. we were declared
|
|
124
|
+
# 'crashed' on a stale heartbeat and the driver re-delegated), or we could
|
|
125
|
+
# not read our own identity at boot (my_token nil), do NOT run: the new
|
|
126
|
+
# worker owns this delegation, and the gate would replay/serialize our stale
|
|
127
|
+
# call anyway. Exiting leaves the current handle to its rightful worker.
|
|
128
|
+
if my_token.nil? || pending['step_token'] != my_token
|
|
129
|
+
exit 0
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Past the guard, boot_identity is non-empty and is our own handle identity
|
|
133
|
+
# (also driving the per-token heartbeat); write_result tags with it.
|
|
134
|
+
identity = boot_identity
|
|
135
|
+
|
|
136
|
+
args = (pending['arguments'] || {}).merge('session_id' => session_id)
|
|
137
|
+
args.delete('execution') # never recurse into another delegation
|
|
138
|
+
|
|
139
|
+
exit 130 if shutdown[:requested]
|
|
140
|
+
|
|
141
|
+
registry = KairosMcp::ToolRegistry.new
|
|
142
|
+
raw = registry.call_tool('agent_step', args)
|
|
143
|
+
|
|
144
|
+
# Normalize the MCP content shape to the response hash the inline call
|
|
145
|
+
# would have returned.
|
|
146
|
+
text = if raw.is_a?(Array) && raw.first.is_a?(Hash)
|
|
147
|
+
raw.first[:text] || raw.first['text']
|
|
148
|
+
end
|
|
149
|
+
response = begin
|
|
150
|
+
text ? JSON.parse(text) : { 'status' => 'error', 'error' => 'unrecognized tool result shape' }
|
|
151
|
+
rescue JSON::ParserError
|
|
152
|
+
{ 'status' => 'error', 'error' => 'unparseable tool result', 'raw' => text.to_s[0, 500] }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Write the result (tagged with our OWN startup identity) and leave the
|
|
156
|
+
# pending handle in place: teardown is the collector's job (agent_wait#collect
|
|
157
|
+
# clears result+handle atomically under delegation.lock), so the worker never
|
|
158
|
+
# races a concurrently-opened fresh delegation by clearing state it may no
|
|
159
|
+
# longer own or by mislabeling its result as a newer delegation's.
|
|
160
|
+
delegation.write_result(response, identity: identity)
|
|
161
|
+
exit 0
|
|
162
|
+
rescue SystemExit, SignalException
|
|
163
|
+
# A deliberate exit (including our own `exit 0`) or a signal is not a
|
|
164
|
+
# failure to report — let it propagate.
|
|
165
|
+
raise
|
|
166
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
167
|
+
# Catch Exception, not just StandardError: LoadError/ScriptError from the
|
|
168
|
+
# bootstrap require are exactly the failure class the driver must see as a
|
|
169
|
+
# result rather than a silently hung handle. Leave teardown to the collector.
|
|
170
|
+
# Tag with our OWN startup identity (boot_identity is always our handle;
|
|
171
|
+
# the in-block `identity` local may be unassigned if we failed early, and
|
|
172
|
+
# a re-read of pending could belong to a superseding delegation).
|
|
173
|
+
begin
|
|
174
|
+
delegation.write_result({ 'status' => 'error', 'error' => "worker: #{e.class}: #{e.message}" },
|
|
175
|
+
identity: (boot_identity.empty? ? nil : boot_identity))
|
|
176
|
+
rescue StandardError
|
|
177
|
+
# best effort
|
|
178
|
+
end
|
|
179
|
+
exit 1
|
|
180
|
+
ensure
|
|
181
|
+
watchdog&.kill
|
|
182
|
+
heartbeat_thread&.kill
|
|
183
|
+
end
|