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

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.

Parameters:

Returns:

  • (Proc)

    ->(orient_output, mandate) → decision Hash



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.

Parameters:

Returns:

  • (Proc)

    ->(observation, mandate) → orient_output Hash



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.

Parameters:

Returns:

  • (Proc)

    ->(act_result, mandate) → reflect_output Hash



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