Class: KairosMcp::Daemon::OodaCycleRunner

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

Overview

OodaCycleRunner — the single callable that Integration.wire! receives as its cycle_runner: parameter.

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

Orchestrates OBSERVE→ORIENT→DECIDE→ACT→REFLECT using all P3.x components.
No global state  all collaborators injected at construction.
Returns the shape Integration expects:
  { status:, llm_calls:, input_tokens:, output_tokens: }

Constant Summary collapse

PAUSED_STATUS =
'paused_awaiting_approval'

Instance Method Summary collapse

Constructor Details

#initialize(workspace_root:, safety:, invoker:, active_observe:, orient_fn:, decide_fn:, reflect_fn:, code_gen_phase_handler:, chain_recorder:, shell:, wal_factory:, usage_accumulator: nil, logger: nil) ⇒ OodaCycleRunner

Returns a new instance of OodaCycleRunner.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/kairos_mcp/daemon/ooda_cycle_runner.rb', line 21

def initialize(
  workspace_root:,
  safety:,
  invoker:,
  active_observe:,
  orient_fn:,
  decide_fn:,
  reflect_fn:,
  code_gen_phase_handler:,
  chain_recorder:,
  shell:,
  wal_factory:,
  usage_accumulator: nil,
  logger: nil
)
  @ws       = workspace_root
  @safety   = safety
  @invoker  = invoker
  @observe  = active_observe
  @orient   = orient_fn
  @decide   = decide_fn
  @reflect  = reflect_fn
  @cg_handler = code_gen_phase_handler
  @chain    = chain_recorder
  @shell    = shell
  @wal_factory = wal_factory
  @logger   = logger
  # Usage accumulator: if orient_fn/decide_fn/reflect_fn use
  # LlmPhaseFunctions with a shared UsageAccumulator, inject it here
  # to enable per-cycle budget tracking. Otherwise falls back to zeros.
  @usage_accumulator = usage_accumulator
end

Instance Method Details

#call(mandate) ⇒ Hash

Returns { status:, llm_calls:, input_tokens:, output_tokens:, phases: }.

Parameters:

  • mandate (Hash)

Returns:

  • (Hash)

    { status:, llm_calls:, input_tokens:, output_tokens:, phases: }



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
82
83
84
85
86
87
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
122
123
124
125
126
# File 'lib/kairos_mcp/daemon/ooda_cycle_runner.rb', line 56

def call(mandate)
  @usage_accumulator&.reset! if @usage_accumulator.respond_to?(:reset!)

  # Flush any pending chain records (always, including resume paths)
  @chain.retry_pending

  # Step 0: Check for pending proposal resume
  resolved = @cg_handler.resume_if_pending
  case resolved
  when nil
    # No pending proposal — proceed with full cycle
  when :still_pending
    return result_hash('paused', phases: [])
  when Hash
    # F1 fix: Open WAL for resume path
    mandate_id = mandate[:id] || mandate['id'] || 'unknown'
    wal = @wal_factory.call(mandate_id)
    cycle = (mandate[:cycles_completed] || mandate['cycles_completed'] || 0) + 1
    recorder = WalPhaseRecorder.new(wal: wal, cycle: cycle)
    begin
      if resolved[:status] == 'applied'
        maybe_run_post_commit(mandate, resolved)
        # F3 fix: chain recording already done by CodeGenAct — don't duplicate
        run_reflect(resolved, mandate, recorder)
        return result_hash('ok', phases: [:resume, :reflect])
      else
        run_reflect(resolved, mandate, recorder)
        return result_hash(resolved[:status], phases: [:resume, :reflect])
      end
    ensure
      wal.close rescue nil if wal.respond_to?(:close)
    end
  end

  # Open WAL
  mandate_id = mandate[:id] || mandate['id'] || 'unknown'
  wal = @wal_factory.call(mandate_id)
  cycle = (mandate[:cycles_completed] || mandate['cycles_completed'] || 0) + 1
  recorder = WalPhaseRecorder.new(wal: wal, cycle: cycle)

  begin
    # Step 1: OBSERVE
    observation = run_observe(mandate, recorder)

    # Step 2: ORIENT
    orient_output = run_orient(observation, mandate, recorder)

    # Step 3: DECIDE
    decision = run_decide(orient_output, mandate, recorder)

    # Step 4: ACT
    act_result = run_act(decision, mandate, recorder)

    if act_result[:status] == PAUSED_STATUS
      return result_hash('paused', phases: [:observe, :orient, :decide, :act],
                         proposal_id: act_result[:proposal_id])
    end

    # Post-commit shell (git add/commit)
    maybe_run_post_commit(mandate, act_result, decision: decision)

    # Chain recording handled by CodeGenAct internally (no duplication)

    # Step 5: REFLECT
    run_reflect(act_result, mandate, recorder)

    result_hash('ok', phases: [:observe, :orient, :decide, :act, :reflect])
  ensure
    wal.close rescue nil if wal.respond_to?(:close)
  end
end