Class: KairosMcp::Daemon::ActiveObserve

Inherits:
Object
  • Object
show all
Defined in:
lib/kairos_mcp/daemon/active_observe.rb

Overview

ActiveObserve — policy-driven OBSERVE phase.

Design (v0.2 §2, P3.1):

The passive OBSERVE phase inspects mandate state in memory. The
active variant additionally invokes a whitelisted set of READ-ONLY
tools named in the mandate's `observe_policies`, collects their
results, and runs a cheap triage step to highlight what looks
relevant. In P3.1 the triage is a keyword filter stub; a future
revision will slot in a cheap LLM call with the same interface.

Safety:

Only tools listed in READ_ONLY_ALLOWLIST (or the caller-supplied
allowlist) are ever invoked. A mandate cannot widen its own
observation surface — policies must be a subset of the allowlist.

Constant Summary collapse

READ_ONLY_ALLOWLIST =

A deliberately conservative default. Additional read-only tools may be allowed by passing an explicit ‘allowlist:` into #initialize.

%w[
  chain_history
  chain_status
  chain_verify
  knowledge_get
  knowledge_list
  skills_list
  skills_get
  skills_dsl_list
  skills_dsl_get
  resource_list
  resource_read
  introspection_health
  introspection_check
  state_status
  state_history
  document_status
  meeting_browse
  meeting_check_freshness
  skillset_browse
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(allowlist: READ_ONLY_ALLOWLIST, keywords: nil, logger: nil) ⇒ ActiveObserve

Returns a new instance of ActiveObserve.



44
45
46
47
48
# File 'lib/kairos_mcp/daemon/active_observe.rb', line 44

def initialize(allowlist: READ_ONLY_ALLOWLIST, keywords: nil, logger: nil)
  @allowlist = allowlist.map(&:to_s).freeze
  @keywords  = keywords
  @logger    = logger
end

Instance Method Details

#observe(mandate_hash, tool_invoker:) ⇒ Hash

Execute the active OBSERVE step.

Parameters:

  • mandate_hash (Hash)

    must expose :observe_policies (or the ‘observe_policies’ key) as an Array of tool names. May expose :goal_name and :goal for keyword-based triage.

  • tool_invoker (#call)

    a callable accepting (tool_name, args) and returning the tool’s native result. The caller supplies this so ActiveObserve itself is I/O-agnostic and trivially testable.

Returns:

  • (Hash)

    structured observation with :policies_invoked, :policies_skipped, :results, :relevant, :errors.

Raises:

  • (ArgumentError)


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/kairos_mcp/daemon/active_observe.rb', line 61

def observe(mandate_hash, tool_invoker:)
  raise ArgumentError, 'mandate_hash required' unless mandate_hash.is_a?(Hash)
  raise ArgumentError, 'tool_invoker must respond to call' unless tool_invoker.respond_to?(:call)

  policies = Array(mandate_hash[:observe_policies] || mandate_hash['observe_policies'])
  # Deduplicate by normalized tool name — first-wins for args if dupes exist.
  policies = policies.uniq { |e| normalize_policy(e).first }
  invoked = []
  skipped = []
  results = {}
  errors  = {}

  policies.each do |entry|
    tool_name, args = normalize_policy(entry)
    unless allowed?(tool_name)
      skipped << tool_name
      log(:warn, :active_observe_skip, tool: tool_name, reason: 'not_in_allowlist')
      next
    end

    begin
      results[tool_name] = tool_invoker.call(tool_name, args)
      invoked << tool_name
    rescue StandardError => e
      errors[tool_name] = "#{e.class}: #{e.message}"
      log(:error, :active_observe_error, tool: tool_name, error: errors[tool_name])
    end
  end

  relevant = select_relevant(results, mandate_hash)

  {
    policies_invoked: invoked,
    policies_skipped: skipped,
    results:          results,
    relevant:         relevant,
    errors:           errors
  }
end

#select_relevant(results, mandate_hash) ⇒ Hash

Triage stub. In P3.1 we score results by simple keyword membership (mandate goal tokens). Returning the full result under a :match entry keeps the interface stable for the eventual LLM-backed implementation, which will add confidence scores without changing the key layout.

Returns:

  • (Hash)

    tool_name → { match: Boolean, score: Float, matched_keywords: […] }



108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/kairos_mcp/daemon/active_observe.rb', line 108

def select_relevant(results, mandate_hash)
  keywords = effective_keywords(mandate_hash)
  return {} if results.empty?

  results.each_with_object({}) do |(tool, payload), acc|
    matched = match_keywords(payload, keywords)
    acc[tool] = {
      match:            !matched.empty? || keywords.empty?,
      score:            keywords.empty? ? 1.0 : matched.size.to_f / keywords.size,
      matched_keywords: matched
    }
  end
end