kairos-chain 3.6.0 → 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 +4 -4
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/agent/config/agent.yml +6 -0
- data/templates/skillsets/agent/test/test_agent_capability_discovery.rb +307 -0
- data/templates/skillsets/agent/tools/agent_step.rb +47 -6
- data/templates/skillsets/document_authoring/config/document_authoring.yml +22 -0
- data/templates/skillsets/document_authoring/knowledge/agent_capability_document_authoring/agent_capability_document_authoring.md +57 -0
- data/templates/skillsets/document_authoring/knowledge/document_authoring_guide/document_authoring_guide.md +64 -0
- data/templates/skillsets/document_authoring/lib/document_authoring/context_assembler.rb +91 -0
- data/templates/skillsets/document_authoring/lib/document_authoring/path_validator.rb +118 -0
- data/templates/skillsets/document_authoring/lib/document_authoring/section_writer.rb +94 -0
- data/templates/skillsets/document_authoring/lib/document_authoring.rb +12 -0
- data/templates/skillsets/document_authoring/skillset.json +19 -0
- data/templates/skillsets/document_authoring/test/test_document_authoring.rb +734 -0
- data/templates/skillsets/document_authoring/tools/document_status.rb +136 -0
- data/templates/skillsets/document_authoring/tools/write_section.rb +203 -0
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70e5ff9eff0a25ceba4e38cf6bdb7f0c7fb45bd1f11b9345148625fe86b48be2
|
|
4
|
+
data.tar.gz: 7de3d8c8b819cc86ab3847a60250ecc98fe1bf66077a10a9f43521910f51480b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d8385510f0309d0705ac2a311da86f05c62332b77cf0f4cc7a0de7d271863f73e9137320d5d7470d5552c0b3da441500c178b7f3d5d8abd6e0b253ce7d562ed
|
|
7
|
+
data.tar.gz: f298792554a96d5da897523d9b6342d3fef2bc06e4d9a86d54a829c611fe5885fe6896168f7fc1853e3565bc2f4e71ed86d3740b39b898aaa4b0afc3c5337f4b
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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,
|
|
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
|
-
"
|
|
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,22 @@
|
|
|
1
|
+
# Document Authoring SkillSet configuration
|
|
2
|
+
llm_model: null # uses llm_client default
|
|
3
|
+
|
|
4
|
+
# Output settings
|
|
5
|
+
output_base_dir: null # null = workspace root (@safety.safe_root)
|
|
6
|
+
|
|
7
|
+
# Per-section limits
|
|
8
|
+
max_words_default: 500
|
|
9
|
+
|
|
10
|
+
# Context assembly
|
|
11
|
+
max_context_chars: 4000 # per source, truncated at paragraph boundary
|
|
12
|
+
max_total_context_chars: 16000 # total context budget
|
|
13
|
+
max_context_sources: 10 # max number of context URIs
|
|
14
|
+
|
|
15
|
+
# File safety
|
|
16
|
+
allowed_output_extensions:
|
|
17
|
+
- ".md"
|
|
18
|
+
- ".txt"
|
|
19
|
+
max_output_file_size_bytes: 1048576 # 1MB — refuse to overwrite larger files
|
|
20
|
+
|
|
21
|
+
# document_status limits
|
|
22
|
+
max_status_files: 50 # max files to scan
|
|
@@ -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).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
tags: [document, authoring, grant, writing, agent, ooda]
|
|
3
|
+
version: "1.0"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Document Authoring Guide
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The `document_authoring` SkillSet provides LLM-driven document section generation
|
|
11
|
+
with L1/L2 context injection. It integrates with the Agent SkillSet's OODA loop
|
|
12
|
+
via autoexec's `internal_execute` dispatcher — no Agent or autoexec changes required.
|
|
13
|
+
|
|
14
|
+
## Tools
|
|
15
|
+
|
|
16
|
+
### write_section
|
|
17
|
+
|
|
18
|
+
Generates a document section using an LLM, with optional context from L1 knowledge
|
|
19
|
+
and L2 session contexts.
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"section_name": "research_significance",
|
|
24
|
+
"instructions": "Explain why genomic data ownership matters for open science",
|
|
25
|
+
"context_sources": ["knowledge://genomicschain_design"],
|
|
26
|
+
"output_file": "grant_draft/02_significance.md",
|
|
27
|
+
"max_words": 500,
|
|
28
|
+
"language": "en"
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### document_status
|
|
33
|
+
|
|
34
|
+
Lists existing draft files with word counts. Non-recursive directory scan.
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"output_dir": "grant_draft/"
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Agent Integration
|
|
43
|
+
|
|
44
|
+
When used with the Agent SkillSet, the workflow is:
|
|
45
|
+
|
|
46
|
+
1. Create L1 knowledge with the document goal (e.g., grant requirements)
|
|
47
|
+
2. Start Agent session: `agent_start(goal_name: "grant_application_uzh")`
|
|
48
|
+
3. Agent ORIENT identifies required sections from the goal
|
|
49
|
+
4. Agent DECIDE generates task steps with `tool_name: "write_section"`
|
|
50
|
+
5. Human approves the plan at [proposed] checkpoint
|
|
51
|
+
6. Agent ACT executes write_section for each section via autoexec
|
|
52
|
+
7. Agent REFLECT evaluates completeness
|
|
53
|
+
|
|
54
|
+
## Context Sources
|
|
55
|
+
|
|
56
|
+
Use the platform URI scheme:
|
|
57
|
+
|
|
58
|
+
- `knowledge://genomicschain_design` — L1 knowledge
|
|
59
|
+
- `context://session_id/context_name` — L2 session context
|
|
60
|
+
|
|
61
|
+
## Output
|
|
62
|
+
|
|
63
|
+
Generated text is written directly to the specified file (no JSON wrapper).
|
|
64
|
+
Files are created under the workspace root with symlink-safe path validation.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KairosMcp
|
|
4
|
+
module SkillSets
|
|
5
|
+
module DocumentAuthoring
|
|
6
|
+
# Retrieves L1/L2 context via resource_read and assembles into prompt text.
|
|
7
|
+
# Uses the platform URI scheme (knowledge://, context://).
|
|
8
|
+
class ContextAssembler
|
|
9
|
+
# @param caller_tool [BaseTool] tool instance with invoke_tool access
|
|
10
|
+
# @param max_chars_per_source [Integer] max characters per source
|
|
11
|
+
# @param max_total_chars [Integer] total context budget
|
|
12
|
+
# @param max_sources [Integer] max number of sources
|
|
13
|
+
def initialize(caller_tool, max_chars_per_source: 4000,
|
|
14
|
+
max_total_chars: 16_000, max_sources: 10)
|
|
15
|
+
@caller = caller_tool
|
|
16
|
+
@max_chars_per_source = max_chars_per_source
|
|
17
|
+
@max_total_chars = max_total_chars
|
|
18
|
+
@max_sources = max_sources
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Assemble context from resource URIs.
|
|
22
|
+
# @param sources [Array<String>] resource URIs (knowledge://, context://)
|
|
23
|
+
# @param context [Object, nil] InvocationContext for policy inheritance
|
|
24
|
+
# @return [Hash] { text: String, loaded: Integer, failed: Integer, warnings: Array<String> }
|
|
25
|
+
def assemble(sources, context: nil)
|
|
26
|
+
return { text: '', loaded: 0, failed: 0, warnings: [] } if sources.nil? || sources.empty?
|
|
27
|
+
|
|
28
|
+
warnings = []
|
|
29
|
+
texts = []
|
|
30
|
+
loaded = 0
|
|
31
|
+
failed = 0
|
|
32
|
+
total_chars = 0
|
|
33
|
+
|
|
34
|
+
if sources.size > @max_sources
|
|
35
|
+
warnings << "Truncated to #{@max_sources} sources (#{sources.size} provided)"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sources.first(@max_sources).each do |uri|
|
|
39
|
+
unless uri.match?(%r{\A(knowledge|context)://})
|
|
40
|
+
warnings << "Unknown URI scheme, skipped: #{uri}"
|
|
41
|
+
failed += 1
|
|
42
|
+
next
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
begin
|
|
46
|
+
result = @caller.invoke_tool('resource_read', { 'uri' => uri }, context: context)
|
|
47
|
+
text = extract_text(result)
|
|
48
|
+
|
|
49
|
+
if text.nil? || text.strip.empty?
|
|
50
|
+
warnings << "Empty content from: #{uri}"
|
|
51
|
+
failed += 1
|
|
52
|
+
next
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
truncated = truncate_at_paragraph(text, @max_chars_per_source)
|
|
56
|
+
remaining = @max_total_chars - total_chars
|
|
57
|
+
truncated = truncated[0...remaining] if truncated.length > remaining
|
|
58
|
+
|
|
59
|
+
texts << "### Source: #{uri}\n#{truncated}"
|
|
60
|
+
total_chars += truncated.length
|
|
61
|
+
loaded += 1
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
warnings << "Failed to load #{uri}: #{e.message}"
|
|
64
|
+
failed += 1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
break if total_chars >= @max_total_chars
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
{ text: texts.join("\n\n"), loaded: loaded, failed: failed, warnings: warnings }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def extract_text(result)
|
|
76
|
+
return '' unless result.is_a?(Array)
|
|
77
|
+
|
|
78
|
+
result.map { |b| b[:text] || b['text'] }.compact.join("\n")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def truncate_at_paragraph(text, max_chars)
|
|
82
|
+
return text if text.length <= max_chars
|
|
83
|
+
|
|
84
|
+
# Find last paragraph break (double newline) before max_chars
|
|
85
|
+
cut = text.rindex("\n\n", max_chars)
|
|
86
|
+
cut && cut > 0 ? text[0..cut] : text[0...max_chars]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|