kairos-chain 3.6.1 → 3.6.2

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: 02a83d13eab2a28deb385acb5e3e57f62204923c2efe4f2768fcf063bca5def2
4
- data.tar.gz: fcc519fed062416470e34d9fa95f1760b92ba61d8fb76b1f568a056e244f8c85
3
+ metadata.gz: 70e5ff9eff0a25ceba4e38cf6bdb7f0c7fb45bd1f11b9345148625fe86b48be2
4
+ data.tar.gz: 7de3d8c8b819cc86ab3847a60250ecc98fe1bf66077a10a9f43521910f51480b
5
5
  SHA512:
6
- metadata.gz: fd5586084ac9d3b6faa446bd7471b9c812dfe6e506d52c6f00471100e8554ce1a7734001d9a0fc8ce715795bf82a606b3592d87d02812d5aeafa0db7ecd64325
7
- data.tar.gz: fb0cc839b19d882bc89c1bcb1b3f95a9737f3d5000eeaf12a0fc6ed2927426d6d7b6d6084d5871a87a6ab362f5b533475d7e94cd13d473b6fb8982b91d82d647
6
+ metadata.gz: 4d8385510f0309d0705ac2a311da86f05c62332b77cf0f4cc7a0de7d271863f73e9137320d5d7470d5552c0b3da441500c178b7f3d5d8abd6e0b253ce7d562ed
7
+ data.tar.gz: f298792554a96d5da897523d9b6342d3fef2bc06e4d9a86d54a829c611fe5885fe6896168f7fc1853e3565bc2f4e71ed86d3740b39b898aaa4b0afc3c5337f4b
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.6.1"
2
+ VERSION = "3.6.2"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -39,5 +39,11 @@ tool_blacklist:
39
39
  - "mcp_connect"
40
40
  - "mcp_disconnect"
41
41
 
42
+ # Additional tools available during ORIENT phase (opt-in from SkillSets).
43
+ # Adding tools here increases the set the LLM can call during ORIENT;
44
+ # the per-phase budget (max_tool_calls) is unchanged.
45
+ orient_tools_extra: []
46
+ # - document_status # uncomment to enable draft checking during ORIENT
47
+
42
48
  # Audit
43
49
  audit_level: summary
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Test suite for Agent Capability Discovery (orient_tools, build_tool_catalog)
5
+ # Usage: ruby test_agent_capability_discovery.rb
6
+
7
+ $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
8
+ $LOAD_PATH.unshift File.expand_path('../../../../lib', __dir__)
9
+
10
+ require 'json'
11
+ require 'yaml'
12
+ require 'fileutils'
13
+ require 'tmpdir'
14
+ require 'kairos_mcp/invocation_context'
15
+ require 'kairos_mcp/tools/base_tool'
16
+ require 'kairos_mcp/tool_registry'
17
+ require_relative '../lib/agent'
18
+ require_relative '../tools/agent_start'
19
+ require_relative '../tools/agent_step'
20
+
21
+ $pass = 0
22
+ $fail = 0
23
+
24
+ def assert(description, &block)
25
+ result = block.call
26
+ if result
27
+ $pass += 1
28
+ puts " PASS: #{description}"
29
+ else
30
+ $fail += 1
31
+ puts " FAIL: #{description}"
32
+ end
33
+ rescue StandardError => e
34
+ $fail += 1
35
+ puts " FAIL: #{description} (#{e.class}: #{e.message})"
36
+ end
37
+
38
+ def section(title)
39
+ puts "\n#{'=' * 60}"
40
+ puts "TEST: #{title}"
41
+ puts '=' * 60
42
+ end
43
+
44
+ TMPDIR = Dir.mktmpdir('agent_cap_test')
45
+
46
+ # Stubs
47
+ module Autonomos
48
+ def self.storage_path(subpath)
49
+ path = File.join(TMPDIR, subpath)
50
+ FileUtils.mkdir_p(path)
51
+ path
52
+ end
53
+
54
+ module Mandate
55
+ def self.create(**kwargs); { mandate_id: 'test_mandate' }; end
56
+ def self.load(id); { mandate_id: id, max_cycles: 3, risk_budget: 'low', recent_gap_descriptions: [] }; end
57
+ def self.save(id, data); end
58
+ def self.update_status(id, status); end
59
+ def self.record_cycle(id, **kwargs); end
60
+ def self.risk_exceeds_budget?(proposal, budget); false; end
61
+ def self.loop_detected?(proposal, gaps); false; end
62
+ def self.check_termination(mandate); nil; end
63
+ end
64
+
65
+ module Ooda
66
+ def observe(goal_name); { 'status' => 'observed' }; end
67
+ end
68
+ end
69
+
70
+ module Autoexec
71
+ def self.config; {}; end
72
+ class TaskDsl
73
+ def self.from_json(json_string)
74
+ data = JSON.parse(json_string, symbolize_names: true)
75
+ data
76
+ end
77
+ end
78
+ end
79
+
80
+ Session = KairosMcp::SkillSets::Agent::Session
81
+
82
+ # =========================================================================
83
+ # Build test infrastructure
84
+ # =========================================================================
85
+
86
+ # Mock registry with some tools
87
+ class CapTestRegistry
88
+ def initialize(tools = [])
89
+ @tools = tools
90
+ end
91
+
92
+ def list_tools
93
+ @tools
94
+ end
95
+
96
+ def call_tool(name, arguments, invocation_context: nil)
97
+ [{ text: '{}' }]
98
+ end
99
+ end
100
+
101
+ # Create an AgentStep instance with custom registry
102
+ def build_agent_step(registry: nil, safety: nil)
103
+ step = KairosMcp::SkillSets::Agent::Tools::AgentStep.new(safety, registry: registry)
104
+ step
105
+ end
106
+
107
+ # Create a session with custom config
108
+ def build_session(config_overrides = {})
109
+ config = {
110
+ 'phases' => {},
111
+ 'tool_blacklist' => %w[agent_* autonomos_*]
112
+ }.merge(config_overrides)
113
+
114
+ ctx = KairosMcp::InvocationContext.new(
115
+ blacklist: config['tool_blacklist'],
116
+ mandate_id: 'test_mandate'
117
+ )
118
+
119
+ Session.new(
120
+ session_id: "test_#{rand(10000)}",
121
+ mandate_id: 'test_mandate',
122
+ goal_name: 'test_goal',
123
+ invocation_context: ctx,
124
+ config: config
125
+ )
126
+ end
127
+
128
+ # =========================================================================
129
+ # 1. orient_tools
130
+ # =========================================================================
131
+
132
+ section "orient_tools"
133
+
134
+ assert("T1: returns base tools when no orient_tools_extra") do
135
+ step = build_agent_step
136
+ session = build_session
137
+ tools = step.send(:orient_tools, session)
138
+ tools.include?('knowledge_list') &&
139
+ tools.include?('resource_read') &&
140
+ tools.size == KairosMcp::SkillSets::Agent::Tools::AgentStep::BASE_ORIENT_TOOLS.size
141
+ end
142
+
143
+ assert("T2: merges orient_tools_extra from config") do
144
+ step = build_agent_step
145
+ session = build_session('orient_tools_extra' => ['document_status', 'custom_tool'])
146
+ tools = step.send(:orient_tools, session)
147
+ tools.include?('document_status') &&
148
+ tools.include?('custom_tool') &&
149
+ tools.include?('knowledge_list')
150
+ end
151
+
152
+ assert("T3: deduplicates entries") do
153
+ step = build_agent_step
154
+ session = build_session('orient_tools_extra' => ['knowledge_list', 'resource_read'])
155
+ tools = step.send(:orient_tools, session)
156
+ tools.count('knowledge_list') == 1
157
+ end
158
+
159
+ assert("handles nil session gracefully") do
160
+ step = build_agent_step
161
+ tools = step.send(:orient_tools, nil)
162
+ tools == KairosMcp::SkillSets::Agent::Tools::AgentStep::BASE_ORIENT_TOOLS
163
+ end
164
+
165
+ # =========================================================================
166
+ # 2. build_tool_catalog
167
+ # =========================================================================
168
+
169
+ section "build_tool_catalog"
170
+
171
+ SAMPLE_TOOLS = [
172
+ { name: 'write_section', description: 'Write a document section',
173
+ inputSchema: { type: 'object', properties: {}, required: %w[section_name instructions output_file] } },
174
+ { name: 'document_status', description: 'Show draft file inventory',
175
+ inputSchema: { type: 'object', properties: {}, required: %w[output_dir] } },
176
+ { name: 'knowledge_get', description: 'Get L1 knowledge',
177
+ inputSchema: { type: 'object', properties: {}, required: %w[name] } },
178
+ { name: 'agent_start', description: 'Start agent session',
179
+ inputSchema: { type: 'object', properties: {}, required: %w[goal_name] } },
180
+ { name: 'autonomos_loop', description: 'Run autonomos loop',
181
+ inputSchema: { type: 'object', properties: {} } }
182
+ ].freeze
183
+
184
+ assert("T4: excludes blacklisted tools") do
185
+ registry = CapTestRegistry.new(SAMPLE_TOOLS)
186
+ step = build_agent_step(registry: registry)
187
+ session = build_session
188
+ catalog = step.send(:build_tool_catalog, session)
189
+ !catalog.include?('agent_start') && !catalog.include?('autonomos_loop')
190
+ end
191
+
192
+ assert("T5: includes non-blacklisted tools with descriptions and required params") do
193
+ registry = CapTestRegistry.new(SAMPLE_TOOLS)
194
+ step = build_agent_step(registry: registry)
195
+ session = build_session
196
+ catalog = step.send(:build_tool_catalog, session)
197
+ catalog.include?('write_section') &&
198
+ catalog.include?('section_name, instructions, output_file') &&
199
+ catalog.include?('document_status') &&
200
+ catalog.include?('knowledge_get')
201
+ end
202
+
203
+ assert("T6: returns fallback when no registry") do
204
+ step = build_agent_step(registry: nil)
205
+ session = build_session
206
+ catalog = step.send(:build_tool_catalog, session)
207
+ catalog.include?('no registry')
208
+ end
209
+
210
+ assert("T7: exact blacklist match works") do
211
+ tools = [{ name: 'skills_evolve', description: 'Evolve skills', inputSchema: {} }]
212
+ registry = CapTestRegistry.new(tools)
213
+ step = build_agent_step(registry: registry)
214
+ session = build_session('tool_blacklist' => %w[skills_evolve])
215
+ catalog = step.send(:build_tool_catalog, session)
216
+ !catalog.include?('skills_evolve')
217
+ end
218
+
219
+ assert("T8: wildcard blacklist via fnmatch works") do
220
+ tools = [
221
+ { name: 'chain_migrate_execute', description: 'Migrate', inputSchema: {} },
222
+ { name: 'chain_history', description: 'History', inputSchema: {} }
223
+ ]
224
+ registry = CapTestRegistry.new(tools)
225
+ step = build_agent_step(registry: registry)
226
+ session = build_session('tool_blacklist' => %w[chain_migrate_*])
227
+ catalog = step.send(:build_tool_catalog, session)
228
+ !catalog.include?('chain_migrate_execute') && catalog.include?('chain_history')
229
+ end
230
+
231
+ assert("handles namespaced tool names (basename blacklist)") do
232
+ tools = [{ name: 'peer1/agent_start', description: 'Remote agent start', inputSchema: {} }]
233
+ registry = CapTestRegistry.new(tools)
234
+ step = build_agent_step(registry: registry)
235
+ session = build_session # blacklist includes agent_*
236
+ catalog = step.send(:build_tool_catalog, session)
237
+ # InvocationContext.allowed? checks basename — should be filtered
238
+ !catalog.include?('peer1/agent_start')
239
+ end
240
+
241
+ # =========================================================================
242
+ # 3. extract_required_params
243
+ # =========================================================================
244
+
245
+ section "extract_required_params"
246
+
247
+ assert("T11: handles symbol keys") do
248
+ step = build_agent_step
249
+ params = step.send(:extract_required_params, { required: %w[name version] })
250
+ params == %w[name version]
251
+ end
252
+
253
+ assert("T11b: handles string keys") do
254
+ step = build_agent_step
255
+ params = step.send(:extract_required_params, { 'required' => %w[output_dir] })
256
+ params == %w[output_dir]
257
+ end
258
+
259
+ assert("handles nil schema") do
260
+ step = build_agent_step
261
+ params = step.send(:extract_required_params, nil)
262
+ params == []
263
+ end
264
+
265
+ assert("handles schema without required") do
266
+ step = build_agent_step
267
+ params = step.send(:extract_required_params, { type: 'object', properties: {} })
268
+ params == []
269
+ end
270
+
271
+ # =========================================================================
272
+ # 4. build_decide_prompt integration
273
+ # =========================================================================
274
+
275
+ section "build_decide_prompt integration"
276
+
277
+ assert("T9: build_decide_prompt includes Available Tools section") do
278
+ registry = CapTestRegistry.new(SAMPLE_TOOLS)
279
+ step = build_agent_step(registry: registry)
280
+ session = build_session
281
+ orient_result = { 'content' => 'The goal requires writing a grant application.' }
282
+ prompt = step.send(:build_decide_prompt, session, orient_result)
283
+ prompt.include?('## Available Tools') &&
284
+ prompt.include?('write_section') &&
285
+ prompt.include?('Use ONLY tools listed above')
286
+ end
287
+
288
+ assert("T10: run_decide_with_feedback messages include catalog") do
289
+ # We can't easily run the full method (needs LLM), but we can verify
290
+ # the method exists and check the code path
291
+ step = build_agent_step(registry: CapTestRegistry.new(SAMPLE_TOOLS))
292
+ session = build_session
293
+ catalog = step.send(:build_tool_catalog, session)
294
+ catalog.include?('write_section') && catalog.include?('document_status')
295
+ end
296
+
297
+ # =========================================================================
298
+ # Cleanup
299
+ # =========================================================================
300
+
301
+ FileUtils.rm_rf(TMPDIR)
302
+
303
+ puts "\n#{'=' * 60}"
304
+ puts "RESULTS: #{$pass} passed, #{$fail} failed (#{$pass + $fail} total)"
305
+ puts '=' * 60
306
+
307
+ exit($fail > 0 ? 1 : 0)
@@ -8,9 +8,9 @@ module KairosMcp
8
8
  module Agent
9
9
  module Tools
10
10
  class AgentStep < KairosMcp::Tools::BaseTool
11
- ORIENT_TOOLS = %w[knowledge_list knowledge_get chain_history
12
- skills_list resource_list resource_read context_save
13
- mcp_list_remote].freeze
11
+ BASE_ORIENT_TOOLS = %w[knowledge_list knowledge_get chain_history
12
+ skills_list resource_list resource_read context_save
13
+ mcp_list_remote].freeze
14
14
 
15
15
  def name
16
16
  'agent_step'
@@ -150,7 +150,7 @@ module KairosMcp
150
150
  orient_prompt = build_orient_prompt(session, observation_text)
151
151
  messages = [{ 'role' => 'user', 'content' => orient_prompt }]
152
152
 
153
- orient_result = loop_inst.run_phase('orient', orient_system_prompt, messages, ORIENT_TOOLS)
153
+ orient_result = loop_inst.run_phase('orient', orient_system_prompt, messages, orient_tools(session))
154
154
  return error_with_state(session, 'observed', orient_result) if orient_result['error']
155
155
 
156
156
  # DECIDE (single-stage; see design v0.4 sec 3.3 for future extension)
@@ -329,11 +329,16 @@ module KairosMcp
329
329
  prior_decision = session.load_decision
330
330
  prior_json = prior_decision ? JSON.generate(prior_decision) : '(none)'
331
331
 
332
+ # Include tool catalog so revise path has same tool awareness as initial DECIDE
333
+ catalog = build_tool_catalog(session)
334
+
332
335
  messages = [
333
336
  { 'role' => 'user', 'content' =>
337
+ "## Available Tools\n#{catalog}\n\n" \
334
338
  "Previous plan:\n#{prior_json}\n\n" \
335
339
  "This plan was rejected. Feedback: #{feedback}\n\n" \
336
- "Please revise the plan and output a new decision_payload as JSON." }
340
+ "Please revise the plan and output a new decision_payload as JSON. " \
341
+ "Use ONLY tools listed above." }
337
342
  ]
338
343
  decide_result = loop_inst.run_decide(decide_system_prompt, messages)
339
344
  return error_with_state(session, 'proposed', decide_result) if decide_result['error']
@@ -442,8 +447,12 @@ module KairosMcp
442
447
 
443
448
  def build_decide_prompt(session, orient_result)
444
449
  analysis = orient_result['content'] || orient_result.to_json
450
+ catalog = build_tool_catalog(session)
451
+
445
452
  "Based on this analysis:\n#{analysis}\n\n" \
446
- "Create a task execution plan as JSON (decision_payload format)."
453
+ "## Available Tools\n#{catalog}\n\n" \
454
+ "Create a task execution plan as JSON (decision_payload format). " \
455
+ "Use ONLY tools listed above."
447
456
  end
448
457
 
449
458
  def build_reflect_prompt(session, act_result)
@@ -452,6 +461,38 @@ module KairosMcp
452
461
  "Evaluate: what was achieved, what remains, confidence level (0.0-1.0)."
453
462
  end
454
463
 
464
+ # ---- Capability Discovery ----
465
+
466
+ # Config-driven ORIENT tools: base + optional extras from agent.yml
467
+ def orient_tools(session)
468
+ extra = session&.config&.dig('orient_tools_extra') || []
469
+ (BASE_ORIENT_TOOLS + extra).uniq
470
+ end
471
+
472
+ # Build a filtered tool catalog for DECIDE prompt.
473
+ # Uses session's InvocationContext.allowed? for blacklist/whitelist
474
+ # consistency with the ACT phase execution policy.
475
+ def build_tool_catalog(session)
476
+ return "(no registry available)" unless @registry
477
+
478
+ ctx = session&.invocation_context
479
+ tools = @registry.list_tools
480
+ tools = tools.reject { |t| ctx && !ctx.allowed?(t[:name]) } if ctx
481
+
482
+ tools.map { |t|
483
+ required = extract_required_params(t[:inputSchema])
484
+ params_str = required.empty? ? '' : " (params: #{required.join(', ')})"
485
+ "- **#{t[:name]}**#{params_str}: #{t[:description]}"
486
+ }.join("\n")
487
+ end
488
+
489
+ # Extract required parameter names from an inputSchema hash.
490
+ def extract_required_params(schema)
491
+ return [] unless schema.is_a?(Hash)
492
+ required = schema[:required] || schema['required'] || []
493
+ required.map(&:to_s)
494
+ end
495
+
455
496
  # ---- Helpers ----
456
497
 
457
498
  def load_last_decision(session)
@@ -0,0 +1,57 @@
1
+ ---
2
+ tags: [agent-capability, document-authoring]
3
+ version: "1.0"
4
+ ---
5
+
6
+ # Agent Capability: Document Authoring
7
+
8
+ ## When to use
9
+
10
+ The user asks to write, draft, or create structured documents such as:
11
+ - Grant applications (e.g., UZH fellowship, SNF, ERC)
12
+ - Research papers or manuscripts
13
+ - Technical reports
14
+ - Project proposals
15
+
16
+ Keywords: write, draft, create, application, grant, paper, report, proposal, document
17
+
18
+ ## Tools
19
+
20
+ - `write_section`: Write a document section using LLM with L1/L2 context injection
21
+ - `document_status`: Check existing draft files and word counts
22
+
23
+ ## Typical task pattern
24
+
25
+ ```json
26
+ {
27
+ "task_id": "write_grant_draft",
28
+ "meta": { "description": "Write grant application sections", "risk_default": "low" },
29
+ "steps": [
30
+ {
31
+ "step_id": "write_abstract",
32
+ "action": "Write project abstract",
33
+ "tool_name": "write_section",
34
+ "tool_arguments": {
35
+ "section_name": "abstract",
36
+ "instructions": "Write a concise project abstract covering motivation, approach, and expected impact",
37
+ "context_sources": ["knowledge://project_description"],
38
+ "output_file": "grant_draft/01_abstract.md",
39
+ "max_words": 250
40
+ },
41
+ "risk": "low", "depends_on": []
42
+ },
43
+ {
44
+ "step_id": "check_status",
45
+ "action": "Verify all sections written",
46
+ "tool_name": "document_status",
47
+ "tool_arguments": { "output_dir": "grant_draft/" },
48
+ "risk": "low", "depends_on": ["write_abstract"]
49
+ }
50
+ ]
51
+ }
52
+ ```
53
+
54
+ ## Context sources
55
+
56
+ Use `knowledge://` URIs for project-level context and `context://` URIs for
57
+ session-specific context (e.g., previous grant feedback).
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kairos-chain
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.1
4
+ version: 3.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masaomi Hatakeyama
@@ -213,6 +213,7 @@ files:
213
213
  - templates/skillsets/agent/lib/agent/message_format.rb
214
214
  - templates/skillsets/agent/lib/agent/session.rb
215
215
  - templates/skillsets/agent/skillset.json
216
+ - templates/skillsets/agent/test/test_agent_capability_discovery.rb
216
217
  - templates/skillsets/agent/test/test_agent_m1.rb
217
218
  - templates/skillsets/agent/test/test_agent_m2.rb
218
219
  - templates/skillsets/agent/test/test_agent_m3.rb
@@ -245,6 +246,7 @@ files:
245
246
  - templates/skillsets/autonomos/tools/autonomos_reflect.rb
246
247
  - templates/skillsets/autonomos/tools/autonomos_status.rb
247
248
  - templates/skillsets/document_authoring/config/document_authoring.yml
249
+ - templates/skillsets/document_authoring/knowledge/agent_capability_document_authoring/agent_capability_document_authoring.md
248
250
  - templates/skillsets/document_authoring/knowledge/document_authoring_guide/document_authoring_guide.md
249
251
  - templates/skillsets/document_authoring/lib/document_authoring.rb
250
252
  - templates/skillsets/document_authoring/lib/document_authoring/context_assembler.rb