kairos-chain 3.7.0 → 3.9.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 +68 -0
- data/lib/kairos_mcp/skillset_manager.rb +79 -16
- data/lib/kairos_mcp/tools/system_upgrade.rb +100 -5
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/agent/config/agent.yml +17 -0
- data/templates/skillsets/agent/lib/agent/cognitive_loop.rb +69 -9
- data/templates/skillsets/agent/lib/agent/mandate_adapter.rb +1 -1
- data/templates/skillsets/agent/lib/agent/session.rb +11 -4
- data/templates/skillsets/agent/tools/agent_execute.rb +311 -0
- data/templates/skillsets/agent/tools/agent_start.rb +60 -9
- data/templates/skillsets/agent/tools/agent_step.rb +493 -71
- data/templates/skillsets/agent/tools/agent_stop.rb +1 -1
- data/templates/skillsets/autonomos/lib/autonomos/mandate.rb +36 -7
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4facdf3bcb070b304f05d2ba2edd1d115e2f743db24c47864d953280da4c80f0
|
|
4
|
+
data.tar.gz: debbc87b1676e18e7a3ff200132d956ac8531324aecbf957e625fc6e8cdab059
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: af08ba6ddefba694090ca8f989b215ae80e2417e794ad3f3ece1bfb8b73e8aa428c1ea636e2ad9278a268b6f95435a180c043a96db15293e2f3351c2c2efc10b
|
|
7
|
+
data.tar.gz: c4d15cc38e425154e64cfd5aa52c80c939c3f00ce4ed8d0036c12e282816c0f320dae27bac46df968a8c74d1ac926037b0883244b7ed64827ff16dec18cc4c84
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,74 @@ 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.9.0] - 2026-03-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **agent_execute** — Claude Code subprocess delegation for file operations
|
|
12
|
+
- Delegates Read/Edit/Write/Glob/Grep to a sandboxed `claude -p` subprocess
|
|
13
|
+
- `--permission-mode acceptEdits` for auto-approval of file edits
|
|
14
|
+
- `--output-format stream-json` for structured result parsing (files_modified, tool_calls)
|
|
15
|
+
- 8 security layers: Agent blacklist, tool restriction, Bash gating (requires
|
|
16
|
+
`Bash(pattern)` in allowed_tools), acceptEdits mode, env scrubbing
|
|
17
|
+
(unsetenv_others: true), project root lock, --max-budget-usd (clamped to
|
|
18
|
+
agent.yml max), external timeout (SIGTERM -> SIGKILL)
|
|
19
|
+
- Configurable via `agent.yml` `agent_execute:` section
|
|
20
|
+
- Design: 2 rounds x 2-3 LLMs. Implementation: 1 round x 3 LLMs.
|
|
21
|
+
|
|
22
|
+
- **Agent ACT routing** — automatic delegation based on task plan
|
|
23
|
+
- `requires_file_operations?` detects Edit/Write/Read/Bash in task steps
|
|
24
|
+
- File operations route to `agent_execute`; MCP tools route to `autoexec_run`
|
|
25
|
+
- Context injection via `--append-system-prompt` (goal + progress)
|
|
26
|
+
|
|
27
|
+
- **SkillSet discovery & install** via `system_upgrade`
|
|
28
|
+
- `system_upgrade command="skillsets"` lists all available SkillSets with status
|
|
29
|
+
- New SkillSets in gem templates auto-detected by `upgrade_check`
|
|
30
|
+
- `system_upgrade command="apply" names=["dream"]` installs specific SkillSets
|
|
31
|
+
- `available_skillsets` method on SkillSetManager
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- `ClaudeCodeAdapter`: removed invalid `--max-turns 1` flag (not in claude CLI)
|
|
36
|
+
- `agent_execute` blacklist: properly removes `agent_*` wildcard + re-adds other agent tools
|
|
37
|
+
- `agent_execute` error propagation: subprocess failures now set `error` key for ACT gates
|
|
38
|
+
|
|
39
|
+
## [3.8.0] - 2026-03-30
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **Agent Autonomous Mode** — Multi-cycle OODA loop execution
|
|
44
|
+
- `agent_start(autonomous: true)`: Enable autonomous mode. Session starts at
|
|
45
|
+
`[observed]` as before; autonomous loop begins on `agent_step(approve)`.
|
|
46
|
+
- 8 safety gates: mandate termination, goal drift detection, wall-clock timeout
|
|
47
|
+
(300s), aggregate LLM budget (60 calls), risk budget, post-ACT termination,
|
|
48
|
+
confidence-based early exit, checkpoint pause.
|
|
49
|
+
- New session states: `autonomous_cycling`, `paused_risk`, `paused_error`
|
|
50
|
+
- Resume handlers: `approve` at `paused_risk` re-checks risk and resumes ACT;
|
|
51
|
+
`approve`/`skip` at `paused_error` skips failed cycle and continues.
|
|
52
|
+
- `agent.yml` autonomous config: `max_total_llm_calls`, `max_duration_seconds`,
|
|
53
|
+
`min_cycles_before_exit`, `confidence_exit_threshold`.
|
|
54
|
+
- Design: 2 rounds x 3 LLMs. Implementation: 1 round x 3 LLMs. All HIGH fixed.
|
|
55
|
+
|
|
56
|
+
- **Mandate locking** — `Mandate.with_lock` for single-writer batch execution
|
|
57
|
+
- File-based exclusive lock (`flock`), non-blocking with `LockError`
|
|
58
|
+
- Atomic save via tmp+rename pattern
|
|
59
|
+
- `Mandate.reload` helper for in-lock refresh
|
|
60
|
+
|
|
61
|
+
- **CognitiveLoop call tracking** — `total_calls` attribute for aggregate
|
|
62
|
+
LLM budget enforcement across autonomous cycles
|
|
63
|
+
|
|
64
|
+
- **Goal drift detection** — Content-based hash (not name-only) at mandate
|
|
65
|
+
creation; per-cycle drift check with fail-open semantics
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- `run_orient_decide` / `run_act_reflect` refactored into `_internal` (Hash return)
|
|
70
|
+
+ wrapper (text_content) pattern. Manual mode behavior unchanged.
|
|
71
|
+
- Manual risk pause now sets session to `paused_risk` (was `terminated`),
|
|
72
|
+
enabling resume via `agent_step(approve)`.
|
|
73
|
+
- `MandateAdapter.to_mandate_proposal` uses `dig` for nil safety.
|
|
74
|
+
|
|
7
75
|
## [3.7.0] - 2026-03-29
|
|
8
76
|
|
|
9
77
|
### Added
|
|
@@ -131,14 +131,18 @@ module KairosMcp
|
|
|
131
131
|
{ success: true, name: installed.name, version: installed.version, layer: installed.layer, path: dest }
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
# Check for available SkillSet upgrades from gem templates
|
|
134
|
+
# Check for available SkillSet upgrades from gem templates.
|
|
135
|
+
# Also detects NEW SkillSets in templates that are not yet installed.
|
|
135
136
|
#
|
|
136
|
-
# @return [Array<Hash>] List of upgradable skillsets with version info
|
|
137
|
+
# @return [Array<Hash>] List of upgradable/new skillsets with version info
|
|
137
138
|
def upgrade_check
|
|
138
139
|
results = []
|
|
139
140
|
templates_dir = File.join(KairosMcp.gem_root, 'templates', 'skillsets')
|
|
140
141
|
return results unless File.directory?(templates_dir)
|
|
141
142
|
|
|
143
|
+
installed_names = all_skillsets.map(&:name)
|
|
144
|
+
|
|
145
|
+
# Check existing installed SkillSets for upgrades
|
|
142
146
|
all_skillsets.each do |installed|
|
|
143
147
|
template_path = File.join(templates_dir, installed.name)
|
|
144
148
|
next unless File.directory?(template_path)
|
|
@@ -157,17 +161,39 @@ module KairosMcp
|
|
|
157
161
|
installed_version: installed.version,
|
|
158
162
|
available_version: template_ss.version,
|
|
159
163
|
version_bump: template_ver > installed_ver,
|
|
160
|
-
changed_files: changed_files
|
|
164
|
+
changed_files: changed_files,
|
|
165
|
+
new_skillset: false
|
|
161
166
|
}
|
|
162
167
|
end
|
|
163
168
|
end
|
|
164
169
|
|
|
170
|
+
# Detect NEW SkillSets in templates not yet installed
|
|
171
|
+
Dir.children(templates_dir).sort.each do |name|
|
|
172
|
+
template_path = File.join(templates_dir, name)
|
|
173
|
+
next unless File.directory?(template_path)
|
|
174
|
+
next if installed_names.include?(name)
|
|
175
|
+
|
|
176
|
+
template_ss = Skillset.new(template_path)
|
|
177
|
+
next unless template_ss.valid?
|
|
178
|
+
|
|
179
|
+
results << {
|
|
180
|
+
name: name,
|
|
181
|
+
installed_version: nil,
|
|
182
|
+
available_version: template_ss.version,
|
|
183
|
+
description: template_ss.description,
|
|
184
|
+
version_bump: false,
|
|
185
|
+
changed_files: [],
|
|
186
|
+
new_skillset: true
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
|
|
165
190
|
results
|
|
166
191
|
end
|
|
167
192
|
|
|
168
|
-
# Apply SkillSet upgrades from gem templates
|
|
193
|
+
# Apply SkillSet upgrades from gem templates.
|
|
194
|
+
# Handles both upgrades (existing) and new installs.
|
|
169
195
|
#
|
|
170
|
-
# @param names [Array<String>, nil] specific names to upgrade, or nil for all
|
|
196
|
+
# @param names [Array<String>, nil] specific names to upgrade/install, or nil for all
|
|
171
197
|
# @return [Array<Hash>] results
|
|
172
198
|
def upgrade_apply(names: nil)
|
|
173
199
|
upgrades = upgrade_check
|
|
@@ -176,24 +202,61 @@ module KairosMcp
|
|
|
176
202
|
results = []
|
|
177
203
|
upgrades.each do |info|
|
|
178
204
|
template_path = File.join(KairosMcp.gem_root, 'templates', 'skillsets', info[:name])
|
|
179
|
-
dest = File.join(@skillsets_dir, info[:name])
|
|
180
205
|
|
|
181
|
-
info[:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
206
|
+
if info[:new_skillset]
|
|
207
|
+
# Install new SkillSet from template
|
|
208
|
+
result = install(template_path)
|
|
209
|
+
results << { name: info[:name], from: nil, to: info[:available_version],
|
|
210
|
+
action: 'installed', files_updated: 0 }
|
|
211
|
+
else
|
|
212
|
+
# Upgrade existing: copy changed files
|
|
213
|
+
dest = File.join(@skillsets_dir, info[:name])
|
|
214
|
+
info[:changed_files].each do |rel_path|
|
|
215
|
+
src = File.join(template_path, rel_path)
|
|
216
|
+
dst = File.join(dest, rel_path)
|
|
217
|
+
FileUtils.mkdir_p(File.dirname(dst))
|
|
218
|
+
FileUtils.cp(src, dst) if File.exist?(src)
|
|
219
|
+
end
|
|
187
220
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
221
|
+
installed = Skillset.new(dest)
|
|
222
|
+
record_skillset_event(installed, 'upgrade')
|
|
223
|
+
results << { name: info[:name], from: info[:installed_version], to: info[:available_version],
|
|
224
|
+
action: 'upgraded', files_updated: info[:changed_files].size }
|
|
225
|
+
end
|
|
192
226
|
end
|
|
193
227
|
|
|
194
228
|
results
|
|
195
229
|
end
|
|
196
230
|
|
|
231
|
+
# List all available SkillSets from gem templates with install status.
|
|
232
|
+
#
|
|
233
|
+
# @return [Array<Hash>] list with name, version, description, installed status
|
|
234
|
+
def available_skillsets
|
|
235
|
+
templates_dir = File.join(KairosMcp.gem_root, 'templates', 'skillsets')
|
|
236
|
+
return [] unless File.directory?(templates_dir)
|
|
237
|
+
|
|
238
|
+
installed_map = all_skillsets.each_with_object({}) { |ss, h| h[ss.name] = ss }
|
|
239
|
+
|
|
240
|
+
Dir.children(templates_dir).sort.filter_map do |name|
|
|
241
|
+
template_path = File.join(templates_dir, name)
|
|
242
|
+
next unless File.directory?(template_path)
|
|
243
|
+
|
|
244
|
+
template_ss = Skillset.new(template_path)
|
|
245
|
+
next unless template_ss.valid?
|
|
246
|
+
|
|
247
|
+
installed = installed_map[name]
|
|
248
|
+
{
|
|
249
|
+
name: name,
|
|
250
|
+
available_version: template_ss.version,
|
|
251
|
+
description: template_ss.description,
|
|
252
|
+
installed: !installed.nil?,
|
|
253
|
+
installed_version: installed&.version,
|
|
254
|
+
enabled: installed ? enabled?(name) : false,
|
|
255
|
+
upgrade_available: installed ? Gem::Version.new(template_ss.version) > Gem::Version.new(installed.version) : false
|
|
256
|
+
}
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
197
260
|
# Remove a SkillSet
|
|
198
261
|
def remove(name)
|
|
199
262
|
skillset = find_skillset(name)
|
|
@@ -57,12 +57,18 @@ module KairosMcp
|
|
|
57
57
|
properties: {
|
|
58
58
|
command: {
|
|
59
59
|
type: 'string',
|
|
60
|
-
description: 'Command: "check", "preview", "apply", or "
|
|
61
|
-
|
|
60
|
+
description: 'Command: "check", "preview", "apply", "status", or "skillsets". ' \
|
|
61
|
+
'"skillsets" lists all available SkillSets with install status.',
|
|
62
|
+
enum: %w[check preview apply status skillsets]
|
|
62
63
|
},
|
|
63
64
|
approved: {
|
|
64
65
|
type: 'boolean',
|
|
65
66
|
description: 'Set to true to approve and apply the upgrade (required for apply command)'
|
|
67
|
+
},
|
|
68
|
+
names: {
|
|
69
|
+
type: 'array',
|
|
70
|
+
items: { type: 'string' },
|
|
71
|
+
description: 'Specific SkillSet names to install/upgrade (optional, for apply command)'
|
|
66
72
|
}
|
|
67
73
|
},
|
|
68
74
|
required: ['command']
|
|
@@ -72,6 +78,7 @@ module KairosMcp
|
|
|
72
78
|
def call(arguments)
|
|
73
79
|
command = arguments['command']
|
|
74
80
|
approved = arguments['approved'] || false
|
|
81
|
+
names = arguments['names']
|
|
75
82
|
|
|
76
83
|
case command
|
|
77
84
|
when 'check'
|
|
@@ -79,11 +86,13 @@ module KairosMcp
|
|
|
79
86
|
when 'preview'
|
|
80
87
|
handle_preview
|
|
81
88
|
when 'apply'
|
|
82
|
-
handle_apply(approved)
|
|
89
|
+
handle_apply(approved, names: names)
|
|
83
90
|
when 'status'
|
|
84
91
|
handle_status
|
|
92
|
+
when 'skillsets'
|
|
93
|
+
handle_skillsets
|
|
85
94
|
else
|
|
86
|
-
text_content("Unknown command: #{command}. Use check, preview, apply, or
|
|
95
|
+
text_content("Unknown command: #{command}. Use check, preview, apply, status, or skillsets.")
|
|
87
96
|
end
|
|
88
97
|
end
|
|
89
98
|
|
|
@@ -122,6 +131,18 @@ module KairosMcp
|
|
|
122
131
|
output += "No upgrade needed. Data directory is up to date.\n"
|
|
123
132
|
end
|
|
124
133
|
|
|
134
|
+
# SkillSet status (always show, even if no L0/L1 upgrade needed)
|
|
135
|
+
ss_info = skillset_upgrade_summary
|
|
136
|
+
if ss_info[:new_count] > 0 || ss_info[:upgrade_count] > 0
|
|
137
|
+
output += "\n### SkillSets\n"
|
|
138
|
+
output += " New available: #{ss_info[:new_count]}\n" if ss_info[:new_count] > 0
|
|
139
|
+
output += " Upgrades available: #{ss_info[:upgrade_count]}\n" if ss_info[:upgrade_count] > 0
|
|
140
|
+
ss_info[:new_names].each { |n| output += " + #{n[:name]} (#{n[:version]}) — #{n[:description]}\n" }
|
|
141
|
+
output += "\nRun `system_upgrade command=\"skillsets\"` for full list.\n"
|
|
142
|
+
output += "Run `system_upgrade command=\"apply\" approved=true` to install all.\n"
|
|
143
|
+
output += "Run `system_upgrade command=\"apply\" approved=true names=[\"dream\"]` to install specific.\n"
|
|
144
|
+
end
|
|
145
|
+
|
|
125
146
|
text_content(output)
|
|
126
147
|
end
|
|
127
148
|
|
|
@@ -207,7 +228,7 @@ module KairosMcp
|
|
|
207
228
|
# =====================================================================
|
|
208
229
|
# apply — Execute the upgrade
|
|
209
230
|
# =====================================================================
|
|
210
|
-
def handle_apply(approved)
|
|
231
|
+
def handle_apply(approved, names: nil)
|
|
211
232
|
unless approved
|
|
212
233
|
return text_content(
|
|
213
234
|
"Upgrade requires approval.\n\n" \
|
|
@@ -318,6 +339,27 @@ module KairosMcp
|
|
|
318
339
|
end
|
|
319
340
|
end
|
|
320
341
|
|
|
342
|
+
# Apply SkillSet upgrades/installs
|
|
343
|
+
begin
|
|
344
|
+
ss_mgr = KairosMcp::SkillSetManager.new
|
|
345
|
+
ss_results = ss_mgr.upgrade_apply(names: names)
|
|
346
|
+
if ss_results.any?
|
|
347
|
+
output += "\n## SkillSets\n\n"
|
|
348
|
+
ss_results.each do |r|
|
|
349
|
+
if r[:action] == 'installed'
|
|
350
|
+
actions[:skillsets_installed] = (actions[:skillsets_installed] || []) << r[:name]
|
|
351
|
+
output += " [INSTALLED] #{r[:name]} v#{r[:to]}\n"
|
|
352
|
+
else
|
|
353
|
+
actions[:skillsets_upgraded] = (actions[:skillsets_upgraded] || []) << r[:name]
|
|
354
|
+
output += " [UPGRADED] #{r[:name]} v#{r[:from]} → v#{r[:to]} (#{r[:files_updated]} files)\n"
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
rescue => e
|
|
359
|
+
output += "\n## SkillSets\n\n"
|
|
360
|
+
output += " [ERROR] SkillSet upgrade failed: #{e.message}\n"
|
|
361
|
+
end
|
|
362
|
+
|
|
321
363
|
# Update .kairos_meta.yml
|
|
322
364
|
update_meta(analyzer.gem_version)
|
|
323
365
|
output += "\n [UPDATED] .kairos_meta.yml → v#{analyzer.gem_version}\n"
|
|
@@ -392,10 +434,63 @@ module KairosMcp
|
|
|
392
434
|
text_content(output)
|
|
393
435
|
end
|
|
394
436
|
|
|
437
|
+
# =====================================================================
|
|
438
|
+
# skillsets — List all available SkillSets with install status
|
|
439
|
+
# =====================================================================
|
|
440
|
+
def handle_skillsets
|
|
441
|
+
ss_mgr = KairosMcp::SkillSetManager.new
|
|
442
|
+
available = ss_mgr.available_skillsets
|
|
443
|
+
|
|
444
|
+
output = "# Available SkillSets\n\n"
|
|
445
|
+
output += "| Name | Available | Installed | Status |\n"
|
|
446
|
+
output += "|------|-----------|-----------|--------|\n"
|
|
447
|
+
|
|
448
|
+
available.each do |ss|
|
|
449
|
+
status = if !ss[:installed]
|
|
450
|
+
'Not installed'
|
|
451
|
+
elsif ss[:upgrade_available]
|
|
452
|
+
"Upgrade: #{ss[:installed_version]} -> #{ss[:available_version]}"
|
|
453
|
+
elsif ss[:enabled]
|
|
454
|
+
'Installed, enabled'
|
|
455
|
+
else
|
|
456
|
+
'Installed, disabled'
|
|
457
|
+
end
|
|
458
|
+
output += "| #{ss[:name]} | #{ss[:available_version]} | #{ss[:installed_version] || '-'} | #{status} |\n"
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
not_installed = available.select { |ss| !ss[:installed] }
|
|
462
|
+
if not_installed.any?
|
|
463
|
+
output += "\n## Not Installed\n\n"
|
|
464
|
+
not_installed.each do |ss|
|
|
465
|
+
output += "- **#{ss[:name]}** v#{ss[:available_version]}"
|
|
466
|
+
output += " — #{ss[:description]}" if ss[:description]
|
|
467
|
+
output += "\n"
|
|
468
|
+
end
|
|
469
|
+
output += "\nTo install: `system_upgrade command=\"apply\" approved=true names=[\"#{not_installed.first[:name]}\"]`\n"
|
|
470
|
+
output += "To install all: `system_upgrade command=\"apply\" approved=true`\n"
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
text_content(output)
|
|
474
|
+
end
|
|
475
|
+
|
|
395
476
|
# =====================================================================
|
|
396
477
|
# Helpers
|
|
397
478
|
# =====================================================================
|
|
398
479
|
|
|
480
|
+
def skillset_upgrade_summary
|
|
481
|
+
ss_mgr = KairosMcp::SkillSetManager.new
|
|
482
|
+
checks = ss_mgr.upgrade_check
|
|
483
|
+
new_ss = checks.select { |c| c[:new_skillset] }
|
|
484
|
+
upgrades = checks.reject { |c| c[:new_skillset] }
|
|
485
|
+
{
|
|
486
|
+
new_count: new_ss.size,
|
|
487
|
+
upgrade_count: upgrades.size,
|
|
488
|
+
new_names: new_ss.map { |s| { name: s[:name], version: s[:available_version], description: s[:description] } }
|
|
489
|
+
}
|
|
490
|
+
rescue StandardError
|
|
491
|
+
{ new_count: 0, upgrade_count: 0, new_names: [] }
|
|
492
|
+
end
|
|
493
|
+
|
|
399
494
|
def pattern_icon(pattern)
|
|
400
495
|
case pattern
|
|
401
496
|
when :unchanged then 'OK'
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -35,6 +35,7 @@ tool_blacklist:
|
|
|
35
35
|
- "challenge_*"
|
|
36
36
|
- "autoexec_plan"
|
|
37
37
|
- "autoexec_run"
|
|
38
|
+
- "agent_execute"
|
|
38
39
|
# MCP client: user-only (requires token)
|
|
39
40
|
- "mcp_connect"
|
|
40
41
|
- "mcp_disconnect"
|
|
@@ -45,5 +46,21 @@ tool_blacklist:
|
|
|
45
46
|
orient_tools_extra: []
|
|
46
47
|
# - document_status # uncomment to enable draft checking during ORIENT
|
|
47
48
|
|
|
49
|
+
# Autonomous mode limits
|
|
50
|
+
autonomous:
|
|
51
|
+
max_total_llm_calls: 60 # across all cycles in one batch
|
|
52
|
+
max_duration_seconds: 300 # wall-clock timeout per batch (5 min)
|
|
53
|
+
min_cycles_before_exit: 2 # confidence exit disabled for first N cycles
|
|
54
|
+
confidence_exit_threshold: 0.9 # minimum confidence for early exit
|
|
55
|
+
|
|
56
|
+
# agent_execute: Claude Code subprocess delegation for file operations
|
|
57
|
+
agent_execute:
|
|
58
|
+
default_tools: ["Read", "Edit", "Write", "Glob", "Grep"]
|
|
59
|
+
default_model: "sonnet"
|
|
60
|
+
default_timeout: 120
|
|
61
|
+
max_timeout: 600
|
|
62
|
+
default_budget_usd: 0.50
|
|
63
|
+
max_budget_usd: 2.00
|
|
64
|
+
|
|
48
65
|
# Audit
|
|
49
66
|
audit_level: summary
|
|
@@ -7,11 +7,17 @@ module KairosMcp
|
|
|
7
7
|
module SkillSets
|
|
8
8
|
module Agent
|
|
9
9
|
class CognitiveLoop
|
|
10
|
+
FALLBACK_PROVIDERS = %w[claude_code].freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :total_calls
|
|
13
|
+
|
|
10
14
|
# @param caller_tool [BaseTool] the agent_step tool instance (has invoke_tool)
|
|
11
15
|
# @param session [Session] current agent session
|
|
12
16
|
def initialize(caller_tool, session)
|
|
13
17
|
@caller = caller_tool
|
|
14
18
|
@session = session
|
|
19
|
+
@fallback_attempted = false
|
|
20
|
+
@total_calls = 0
|
|
15
21
|
end
|
|
16
22
|
|
|
17
23
|
# Generic phase runner for ORIENT, REFLECT, and DECIDE_PREP.
|
|
@@ -29,14 +35,13 @@ module KairosMcp
|
|
|
29
35
|
'stop_reason' => 'budget' }
|
|
30
36
|
end
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
@total_calls += 1
|
|
39
|
+
parsed = call_llm_with_fallback(
|
|
33
40
|
'messages' => messages,
|
|
34
41
|
'system' => system_prompt,
|
|
35
42
|
'tools' => available_tools,
|
|
36
43
|
'invocation_context_json' => @session.invocation_context.to_json
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
parsed = JSON.parse(llm_result.map { |b| b[:text] || b['text'] }.compact.join)
|
|
44
|
+
)
|
|
40
45
|
return { 'error' => parsed['error'] } if parsed['status'] == 'error'
|
|
41
46
|
|
|
42
47
|
response = parsed['response']
|
|
@@ -79,14 +84,14 @@ module KairosMcp
|
|
|
79
84
|
return { 'error' => 'Budget exceeded for DECIDE phase' }
|
|
80
85
|
end
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
@total_calls += 1
|
|
88
|
+
|
|
89
|
+
parsed = call_llm_with_fallback(
|
|
83
90
|
'messages' => messages,
|
|
84
91
|
'system' => system_prompt,
|
|
85
92
|
'tools' => [],
|
|
86
93
|
'invocation_context_json' => @session.invocation_context.to_json
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
parsed = JSON.parse(llm_result.map { |b| b[:text] || b['text'] }.compact.join)
|
|
94
|
+
)
|
|
90
95
|
return { 'error' => parsed['error'] } if parsed['status'] == 'error'
|
|
91
96
|
|
|
92
97
|
response = parsed['response']
|
|
@@ -110,7 +115,7 @@ module KairosMcp
|
|
|
110
115
|
begin
|
|
111
116
|
decision = JSON.parse(json_str)
|
|
112
117
|
task_json_str = JSON.generate(decision['task_json'])
|
|
113
|
-
Autoexec::TaskDsl.from_json(task_json_str)
|
|
118
|
+
::Autoexec::TaskDsl.from_json(task_json_str)
|
|
114
119
|
return { 'decision_payload' => decision }
|
|
115
120
|
rescue => e
|
|
116
121
|
if attempts >= max_repair
|
|
@@ -127,6 +132,61 @@ module KairosMcp
|
|
|
127
132
|
|
|
128
133
|
private
|
|
129
134
|
|
|
135
|
+
# Call llm_call with automatic provider fallback on auth errors.
|
|
136
|
+
# Tries the configured provider first. On auth_error, switches to
|
|
137
|
+
# fallback providers (claude_code) via llm_configure, then retries once.
|
|
138
|
+
def call_llm_with_fallback(arguments)
|
|
139
|
+
llm_result = @caller.invoke_tool('llm_call', arguments,
|
|
140
|
+
context: @session.invocation_context)
|
|
141
|
+
parsed = JSON.parse(llm_result.map { |b| b[:text] || b['text'] }.compact.join)
|
|
142
|
+
|
|
143
|
+
# If not an auth error, or already tried fallback, return as-is
|
|
144
|
+
error_info = parsed['error']
|
|
145
|
+
if !error_info || !error_info.is_a?(Hash) || error_info['type'] != 'auth_error' || @fallback_attempted
|
|
146
|
+
return parsed
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Attempt provider fallback
|
|
150
|
+
original_provider = error_info['provider'] || 'configured'
|
|
151
|
+
warn "[agent] Auth error from #{original_provider}, attempting provider fallback"
|
|
152
|
+
|
|
153
|
+
FALLBACK_PROVIDERS.each do |fallback|
|
|
154
|
+
@fallback_attempted = true
|
|
155
|
+
configure_result = try_configure_provider(fallback)
|
|
156
|
+
next unless configure_result
|
|
157
|
+
|
|
158
|
+
warn "[agent] Switched to provider: #{fallback}"
|
|
159
|
+
retry_result = @caller.invoke_tool('llm_call', arguments,
|
|
160
|
+
context: @session.invocation_context)
|
|
161
|
+
retry_parsed = JSON.parse(retry_result.map { |b| b[:text] || b['text'] }.compact.join)
|
|
162
|
+
|
|
163
|
+
# If this provider also fails with auth_error, try next
|
|
164
|
+
retry_error = retry_parsed['error']
|
|
165
|
+
if retry_error.is_a?(Hash) && retry_error['type'] == 'auth_error'
|
|
166
|
+
warn "[agent] Fallback provider #{fallback} also failed: #{retry_error['message']}"
|
|
167
|
+
next
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
return retry_parsed
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# All fallbacks exhausted — return original error with fallback info
|
|
174
|
+
parsed['error']['fallback_attempted'] = true
|
|
175
|
+
parsed['error']['fallback_exhausted'] = true
|
|
176
|
+
parsed
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def try_configure_provider(provider)
|
|
180
|
+
args = { 'provider' => provider }
|
|
181
|
+
result = @caller.invoke_tool('llm_configure', args,
|
|
182
|
+
context: @session.invocation_context)
|
|
183
|
+
parsed = JSON.parse(result.map { |b| b[:text] || b['text'] }.compact.join)
|
|
184
|
+
parsed['status'] == 'ok'
|
|
185
|
+
rescue StandardError => e
|
|
186
|
+
warn "[agent] Failed to configure provider #{provider}: #{e.message}"
|
|
187
|
+
false
|
|
188
|
+
end
|
|
189
|
+
|
|
130
190
|
def extract_json(content)
|
|
131
191
|
JSON.parse(content)
|
|
132
192
|
content
|
|
@@ -11,7 +11,7 @@ module KairosMcp
|
|
|
11
11
|
def self.to_mandate_proposal(decision_payload)
|
|
12
12
|
{
|
|
13
13
|
autoexec_task: {
|
|
14
|
-
steps: (decision_payload
|
|
14
|
+
steps: (decision_payload.dig('task_json', 'steps') || []).map { |s|
|
|
15
15
|
{ risk: s['risk'] || 'low', tool_name: s['tool_name'] }
|
|
16
16
|
}
|
|
17
17
|
},
|
|
@@ -8,19 +8,25 @@ module KairosMcp
|
|
|
8
8
|
module Agent
|
|
9
9
|
class Session
|
|
10
10
|
attr_reader :session_id, :mandate_id, :goal_name, :invocation_context,
|
|
11
|
-
:state, :cycle_number, :config
|
|
11
|
+
:state, :cycle_number, :config, :autonomous
|
|
12
12
|
|
|
13
|
-
def initialize(session_id:, mandate_id:, goal_name:, invocation_context:, config
|
|
13
|
+
def initialize(session_id:, mandate_id:, goal_name:, invocation_context:, config:,
|
|
14
|
+
autonomous: false)
|
|
14
15
|
@session_id = session_id
|
|
15
16
|
@mandate_id = mandate_id
|
|
16
17
|
@goal_name = goal_name
|
|
17
18
|
@invocation_context = invocation_context
|
|
18
19
|
@config = config
|
|
20
|
+
@autonomous = autonomous
|
|
19
21
|
@state = 'created'
|
|
20
22
|
@cycle_number = 0
|
|
21
23
|
@snapshots = []
|
|
22
24
|
end
|
|
23
25
|
|
|
26
|
+
def autonomous?
|
|
27
|
+
@autonomous == true
|
|
28
|
+
end
|
|
29
|
+
|
|
24
30
|
# Per-phase budget configuration.
|
|
25
31
|
# Returns defaults if the phase is not configured.
|
|
26
32
|
def phase_config(phase_name)
|
|
@@ -109,7 +115,7 @@ module KairosMcp
|
|
|
109
115
|
data = {
|
|
110
116
|
session_id: @session_id, mandate_id: @mandate_id,
|
|
111
117
|
goal_name: @goal_name, state: @state, cycle_number: @cycle_number,
|
|
112
|
-
config: @config,
|
|
118
|
+
config: @config, autonomous: @autonomous,
|
|
113
119
|
invocation_context: @invocation_context.to_h
|
|
114
120
|
}
|
|
115
121
|
File.write(state_path, JSON.pretty_generate(data))
|
|
@@ -133,7 +139,8 @@ module KairosMcp
|
|
|
133
139
|
mandate_id: data['mandate_id'],
|
|
134
140
|
goal_name: data['goal_name'],
|
|
135
141
|
invocation_context: ctx,
|
|
136
|
-
config: data['config']
|
|
142
|
+
config: data['config'],
|
|
143
|
+
autonomous: data['autonomous'] || false
|
|
137
144
|
)
|
|
138
145
|
session.instance_variable_set(:@state, data['state'])
|
|
139
146
|
session.instance_variable_set(:@cycle_number, data['cycle_number'] || 0)
|