Module: KairosMcp::Daemon::LlmPhaseFunctions
- Defined in:
- lib/kairos_mcp/daemon/llm_phase_functions.rb
Overview
LlmPhaseFunctions — lightweight LLM callables for OodaCycleRunner.
Design (Technical Debt #1+#2 resolution):
Instead of integrating with the full Autonomos CognitiveLoop,
these are thin wrappers around llm_call (MCP tool or direct callable).
Each function receives observation/orient data and returns structured output.
Usage tracking: each call records input/output tokens into a shared
UsageAccumulator that the runner can query.
The llm_caller is a callable: ->(messages:, system:, max_tokens:, **) → Hash It must return: { content: String, input_tokens: Int, output_tokens: Int }
Defined Under Namespace
Classes: UsageAccumulator
Class Method Summary collapse
-
.call_and_record(llm_caller, usage, **kwargs) ⇒ Object
Call llm_caller and record usage.
-
.decide_fn(llm_caller:, usage:, workspace_root:, max_tokens: 2048) ⇒ Proc
Build decide_fn callable.
-
.orient_fn(llm_caller:, usage:, max_tokens: 1024) ⇒ Proc
Build orient_fn callable.
- .parse_json_response(response, fallback:) ⇒ Object
-
.reflect_fn(llm_caller:, usage:, max_tokens: 512) ⇒ Proc
Build reflect_fn callable.
- .symbolize_keys(hash) ⇒ Object
Class Method Details
.call_and_record(llm_caller, usage, **kwargs) ⇒ Object
Call llm_caller and record usage. On failure, record the failed attempts into usage before re-raising so Budget stays accurate.
155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/kairos_mcp/daemon/llm_phase_functions.rb', line 155 def self.call_and_record(llm_caller, usage, **kwargs) response = llm_caller.call(**kwargs) usage.record(response) response rescue StandardError => e # Record failed attempts if the error carries attempt count if e.respond_to?(:attempts) && e.attempts.is_a?(Integer) && e.attempts > 0 usage.record({ attempts: e.attempts, input_tokens: 0, output_tokens: 0 }) end raise end |
.decide_fn(llm_caller:, usage:, workspace_root:, max_tokens: 2048) ⇒ Proc
Build decide_fn callable.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/kairos_mcp/daemon/llm_phase_functions.rb', line 88 def self.decide_fn(llm_caller:, usage:, workspace_root:, max_tokens: 2048) lambda do |orient_output, mandate| goal = mandate[:goal] || mandate['goal'] || mandate[:goal_name] || mandate['goal_name'] || 'general maintenance' scope_hint = mandate[:decide_hints] || mandate['decide_hints'] || {} prompt = <<~PROMPT You are an autonomous agent in the DECIDE phase of an OODA loop. Your goal: #{goal} Workspace: #{workspace_root} ORIENTATION: #{JSON.pretty_generate(orient_output)[0, 2000]} SCOPE CONSTRAINTS: - Preferred scope: #{scope_hint[:prefer_scope] || scope_hint['prefer_scope'] || 'L2'} - Max edit bytes: #{scope_hint[:max_edit_bytes] || scope_hint['max_edit_bytes'] || 4096} DECIDE what action to take. Return JSON with one of: 1. {"action": "code_edit", "target": "relative/path.md", "old_string": "exact text to replace", "new_string": "replacement text", "intent": "why"} 2. {"action": "noop", "reason": "why no action needed"} IMPORTANT: old_string must be an EXACT substring currently in the file. Target path must be relative to workspace root. PROMPT response = call_and_record(llm_caller, usage, messages: [{ role: 'user', content: prompt }], system: 'You are a KairosChain daemon agent. Return only valid JSON.', max_tokens: max_tokens ) decision = parse_json_response(response, fallback: { action: 'noop', reason: 'LLM parse failure' }) symbolize_keys(decision) end end |
.orient_fn(llm_caller:, usage:, max_tokens: 1024) ⇒ Proc
Build orient_fn callable.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/kairos_mcp/daemon/llm_phase_functions.rb', line 54 def self.orient_fn(llm_caller:, usage:, max_tokens: 1024) lambda do |observation, mandate| goal = mandate[:goal] || mandate['goal'] || mandate[:goal_name] || mandate['goal_name'] || 'general maintenance' relevant = observation[:relevant] || observation['relevant'] || {} results = observation[:results] || observation['results'] || {} prompt = <<~PROMPT You are an autonomous agent in the ORIENT phase of an OODA loop. Your goal: #{goal} OBSERVATION RESULTS: #{JSON.pretty_generate(results)[0, 2000]} RELEVANT SIGNALS: #{JSON.pretty_generate(relevant)[0, 1000]} Analyze the observations and produce a structured orientation. Return JSON with keys: summary (string), priorities (array of strings), risk_level (low/medium/high). PROMPT response = call_and_record(llm_caller, usage, messages: [{ role: 'user', content: prompt }], system: 'You are a KairosChain daemon agent. Return only valid JSON.', max_tokens: max_tokens ) parse_json_response(response, fallback: { summary: 'no orientation', priorities: [], risk_level: 'low' }) end end |
.parse_json_response(response, fallback:) ⇒ Object
167 168 169 170 171 172 173 174 175 176 |
# File 'lib/kairos_mcp/daemon/llm_phase_functions.rb', line 167 def self.parse_json_response(response, fallback:) content = response[:content] || response['content'] || '' # Extract JSON from markdown fences if present if content.include?('```') content = content.gsub(/```(?:json)?\s*/, '').gsub(/```/, '').strip end JSON.parse(content) rescue JSON::ParserError fallback end |
.reflect_fn(llm_caller:, usage:, max_tokens: 512) ⇒ Proc
Build reflect_fn callable.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/kairos_mcp/daemon/llm_phase_functions.rb', line 127 def self.reflect_fn(llm_caller:, usage:, max_tokens: 512) lambda do |act_result, mandate| prompt = <<~PROMPT You are an autonomous agent in the REFLECT phase of an OODA loop. Goal: #{mandate[:goal] || mandate['goal'] || 'maintenance'} ACT RESULT: #{JSON.pretty_generate(act_result)[0, 1500]} Reflect on the outcome. Return JSON with: - assessment: "success" | "partial" | "failure" - lessons: array of strings (what to improve next cycle) - confidence: 0.0 to 1.0 PROMPT response = call_and_record(llm_caller, usage, messages: [{ role: 'user', content: prompt }], system: 'You are a KairosChain daemon agent. Return only valid JSON.', max_tokens: max_tokens ) parse_json_response(response, fallback: { assessment: 'unknown', lessons: [], confidence: 0.5 }) end end |
.symbolize_keys(hash) ⇒ Object
178 179 180 181 |
# File 'lib/kairos_mcp/daemon/llm_phase_functions.rb', line 178 def self.symbolize_keys(hash) return hash unless hash.is_a?(Hash) hash.transform_keys(&:to_sym) end |