kairos-chain 3.6.2 → 3.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70e5ff9eff0a25ceba4e38cf6bdb7f0c7fb45bd1f11b9345148625fe86b48be2
4
- data.tar.gz: 7de3d8c8b819cc86ab3847a60250ecc98fe1bf66077a10a9f43521910f51480b
3
+ metadata.gz: b7d045865d5f35b2ceba8abf858cc33219f88690feaf5591183b20154cda36e4
4
+ data.tar.gz: f333e43070e2268da45be99949d3c85217bfcf2a5d7d691e707fbe26f5b96796
5
5
  SHA512:
6
- metadata.gz: 4d8385510f0309d0705ac2a311da86f05c62332b77cf0f4cc7a0de7d271863f73e9137320d5d7470d5552c0b3da441500c178b7f3d5d8abd6e0b253ce7d562ed
7
- data.tar.gz: f298792554a96d5da897523d9b6342d3fef2bc06e4d9a86d54a829c611fe5885fe6896168f7fc1853e3565bc2f4e71ed86d3740b39b898aaa4b0afc3c5337f4b
6
+ metadata.gz: 4951cb6216a2f85212c12882c02337fc86e2a8abd906bcfd1322ca7d23760226692608a92626ea807c7a67b44c3f94b6eadd96ef88a6125a551dcca30a951954
7
+ data.tar.gz: ccf714bf15d9b54219373f9a83560943fcff476cb700a459cf5d62b5c02c02ce87c0986bf81d8b5b3d7c9708b1eb58c94c5e1ecb96c5c5909ec9bf6f562c1d78
data/CHANGELOG.md CHANGED
@@ -4,6 +4,71 @@ 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.8.0] - 2026-03-30
8
+
9
+ ### Added
10
+
11
+ - **Agent Autonomous Mode** — Multi-cycle OODA loop execution
12
+ - `agent_start(autonomous: true)`: Enable autonomous mode. Session starts at
13
+ `[observed]` as before; autonomous loop begins on `agent_step(approve)`.
14
+ - 8 safety gates: mandate termination, goal drift detection, wall-clock timeout
15
+ (300s), aggregate LLM budget (60 calls), risk budget, post-ACT termination,
16
+ confidence-based early exit, checkpoint pause.
17
+ - New session states: `autonomous_cycling`, `paused_risk`, `paused_error`
18
+ - Resume handlers: `approve` at `paused_risk` re-checks risk and resumes ACT;
19
+ `approve`/`skip` at `paused_error` skips failed cycle and continues.
20
+ - `agent.yml` autonomous config: `max_total_llm_calls`, `max_duration_seconds`,
21
+ `min_cycles_before_exit`, `confidence_exit_threshold`.
22
+ - Design: 2 rounds x 3 LLMs. Implementation: 1 round x 3 LLMs. All HIGH fixed.
23
+
24
+ - **Mandate locking** — `Mandate.with_lock` for single-writer batch execution
25
+ - File-based exclusive lock (`flock`), non-blocking with `LockError`
26
+ - Atomic save via tmp+rename pattern
27
+ - `Mandate.reload` helper for in-lock refresh
28
+
29
+ - **CognitiveLoop call tracking** — `total_calls` attribute for aggregate
30
+ LLM budget enforcement across autonomous cycles
31
+
32
+ - **Goal drift detection** — Content-based hash (not name-only) at mandate
33
+ creation; per-cycle drift check with fail-open semantics
34
+
35
+ ### Changed
36
+
37
+ - `run_orient_decide` / `run_act_reflect` refactored into `_internal` (Hash return)
38
+ + wrapper (text_content) pattern. Manual mode behavior unchanged.
39
+ - Manual risk pause now sets session to `paused_risk` (was `terminated`),
40
+ enabling resume via `agent_step(approve)`.
41
+ - `MandateAdapter.to_mandate_proposal` uses `dig` for nil safety.
42
+
43
+ ## [3.7.0] - 2026-03-29
44
+
45
+ ### Added
46
+
47
+ - **Dream SkillSet** — L2 memory consolidation and lifecycle management
48
+ - `dream_scan`: Pattern detection across L2 sessions — tag co-occurrence,
49
+ L2/L1 staleness (mtime-based), name overlap (Jaccard), archive candidate detection.
50
+ Filters soft-archived stubs from promotion candidates.
51
+ - `dream_archive`: L2 soft-archive — gzip compress .md, move full context directory
52
+ to archive, leave searchable stub (tags + summary). SHA256 verified inline.
53
+ Per-context flock. `dry_run: true` by default.
54
+ - `dream_recall`: Restore archived contexts with SHA256 integrity check.
55
+ Preview and verify-only modes (read-only, no permission required).
56
+ - `dream_propose`: Package L1 promotion proposals with ready-to-execute
57
+ `knowledge_update` commands. Optional Persona Assembly templates.
58
+ - L2 lifecycle model: Active → Candidate → Soft-Archived → Recalled
59
+ - `dream_trigger_policy` L1 knowledge for Kairotic trigger heuristics
60
+ - 119 tests across 27 test sections
61
+
62
+ - **Agent SkillSet — permission advisory**
63
+ - `agent_start` now includes `permission_advisory` in response,
64
+ recommending users configure permission mode (Normal / Auto-allow / Auto-accept)
65
+ for smoother autonomous operation
66
+
67
+ ### Fixed
68
+
69
+ - **L1 staleness detection** — use tag overlap and name token matching instead of
70
+ exact L1 name-in-L2-tags check. Reduces false positives from 48/48 to 7/48.
71
+
7
72
  ## [3.6.0] - 2026-03-28
8
73
 
9
74
  ### Added
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.6.2"
2
+ VERSION = "3.8.0"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -45,5 +45,12 @@ tool_blacklist:
45
45
  orient_tools_extra: []
46
46
  # - document_status # uncomment to enable draft checking during ORIENT
47
47
 
48
+ # Autonomous mode limits
49
+ autonomous:
50
+ max_total_llm_calls: 60 # across all cycles in one batch
51
+ max_duration_seconds: 300 # wall-clock timeout per batch (5 min)
52
+ min_cycles_before_exit: 2 # confidence exit disabled for first N cycles
53
+ confidence_exit_threshold: 0.9 # minimum confidence for early exit
54
+
48
55
  # Audit
49
56
  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
- llm_result = @caller.invoke_tool('llm_call', {
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
- }, context: @session.invocation_context)
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
- llm_result = @caller.invoke_tool('llm_call', {
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
- }, context: @session.invocation_context)
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['task_json']['steps'] || []).map { |s|
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)
@@ -52,6 +52,12 @@ module KairosMcp
52
52
  risk_budget: {
53
53
  type: 'string',
54
54
  description: 'Maximum risk level: "low" or "medium" (default: "low")'
55
+ },
56
+ autonomous: {
57
+ type: 'boolean',
58
+ description: 'Enable autonomous mode (default: false). ' \
59
+ 'Session starts at [observed] regardless. ' \
60
+ 'Autonomous loop begins on first agent_step(approve).'
55
61
  }
56
62
  },
57
63
  required: ['goal_name']
@@ -63,10 +69,14 @@ module KairosMcp
63
69
  max_cycles = arguments['max_cycles'] || 3
64
70
  checkpoint_every = arguments['checkpoint_every'] || 1
65
71
  risk_budget = arguments['risk_budget'] || 'low'
72
+ autonomous = arguments['autonomous'] == true
73
+
74
+ # Pre-resolve goal content for content-based drift detection hash
75
+ pre_obs = run_observe(goal_name)
76
+ goal_content_for_hash = pre_obs['goal_content'] || goal_name
77
+ goal_hash = Digest::SHA256.hexdigest(goal_content_for_hash)[0..15]
66
78
 
67
- # Create mandate via Autonomos
68
- goal_hash = Digest::SHA256.hexdigest(goal_name)[0..15]
69
- mandate = Autonomos::Mandate.create(
79
+ mandate = ::Autonomos::Mandate.create(
70
80
  goal_name: goal_name,
71
81
  goal_hash: goal_hash,
72
82
  max_cycles: max_cycles,
@@ -85,23 +95,30 @@ module KairosMcp
85
95
  mandate_id: mandate[:mandate_id],
86
96
  goal_name: goal_name,
87
97
  invocation_context: ctx,
88
- config: config
98
+ config: config,
99
+ autonomous: autonomous
89
100
  )
90
101
 
91
- # OBSERVE (no LLM direct Ruby)
92
- observation = run_observe(goal_name)
102
+ # OBSERVE: reuse pre-resolved observation (avoids duplicate L2/L1 lookups)
103
+ observation = pre_obs
93
104
 
94
105
  session.save_observation(observation)
95
106
  session.update_state('observed')
96
107
  session.save
97
108
 
98
- text_content(JSON.generate({
109
+ result = {
99
110
  'status' => 'ok',
100
111
  'session_id' => session_id,
101
112
  'mandate_id' => mandate[:mandate_id],
102
113
  'state' => 'observed',
114
+ 'autonomous' => autonomous,
103
115
  'observation' => observation
104
- }))
116
+ }
117
+
118
+ # Advisory: suggest permission mode for autonomous operation
119
+ result['permission_advisory'] = permission_advisory_message
120
+
121
+ text_content(JSON.generate(result))
105
122
  rescue ArgumentError => e
106
123
  text_content(JSON.generate({ 'status' => 'error', 'error' => e.message }))
107
124
  rescue StandardError => e
@@ -126,14 +143,30 @@ module KairosMcp
126
143
  )
127
144
  end
128
145
 
146
+ def permission_advisory_message
147
+ <<~MSG.strip
148
+ This agent session will execute tools autonomously.
149
+ For smoother operation, consider adjusting your permission mode:
150
+
151
+ 1. Normal (default) — ask for each command. Safest, but interrupts flow.
152
+ 2. Auto-allow — pre-approved commands only. Balanced.
153
+ Configure in .claude/settings.local.json permissions.allow array.
154
+ 3. Auto-accept — allow everything. Fastest for trusted tasks.
155
+ Run /permissions and select auto mode, or start with --dangerously-skip-permissions.
156
+
157
+ Recommendation: For implementation + multi-LLM review workflows, auto-allow
158
+ with ruby/codex/agent commands pre-approved provides the best balance.
159
+ MSG
160
+ end
161
+
129
162
  def run_observe(goal_name)
130
163
  # Gather observation data without LLM
131
164
  observation = { 'goal_name' => goal_name, 'timestamp' => Time.now.iso8601 }
132
165
 
133
- # Try to load goal from L2/L1
134
- if defined?(Autonomos::Ooda)
166
+ # Load environment data via Autonomos::Ooda
167
+ if defined?(::Autonomos::Ooda)
135
168
  begin
136
- helper = Class.new { include Autonomos::Ooda }.new
169
+ helper = Class.new { include ::Autonomos::Ooda }.new
137
170
  ooda_obs = helper.observe(goal_name)
138
171
  observation.merge!(ooda_obs.transform_keys(&:to_s)) if ooda_obs.is_a?(Hash)
139
172
  rescue StandardError => e
@@ -141,8 +174,47 @@ module KairosMcp
141
174
  end
142
175
  end
143
176
 
177
+ # Load goal content from L2/L1 so Orient has context to analyze
178
+ begin
179
+ if defined?(::Autonomos::Ooda)
180
+ helper = Class.new { include ::Autonomos::Ooda }.new
181
+ goal = helper.load_goal(goal_name)
182
+ else
183
+ goal = load_goal_fallback(goal_name)
184
+ end
185
+ if goal && goal[:found]
186
+ observation['goal_content'] = goal[:content]
187
+ observation['goal_source'] = goal[:source].to_s
188
+ end
189
+ rescue StandardError => e
190
+ observation['goal_load_error'] = e.message
191
+ end
192
+
144
193
  observation
145
194
  end
195
+
196
+ def load_goal_fallback(goal_name)
197
+ # Direct L2/L1 lookup when Autonomos::Ooda is unavailable
198
+ if defined?(KairosMcp::ContextManager)
199
+ ctx_mgr = KairosMcp::ContextManager.new
200
+ ctx_mgr.list_sessions.each do |session|
201
+ entry = ctx_mgr.get_context(session[:session_id], goal_name)
202
+ if entry && entry.respond_to?(:content) && entry.content && !entry.content.strip.empty?
203
+ return { content: entry.content, found: true, source: :l2 }
204
+ end
205
+ end
206
+ end
207
+ if defined?(KairosMcp::KnowledgeProvider)
208
+ provider = KairosMcp::KnowledgeProvider.new(nil)
209
+ result = provider.get(goal_name)
210
+ if result && result[:content] && !result[:content].strip.empty?
211
+ return { content: result[:content], found: true, source: :l1 }
212
+ end
213
+ end
214
+ { content: nil, found: false }
215
+ rescue StandardError
216
+ { content: nil, found: false }
217
+ end
146
218
  end
147
219
  end
148
220
  end