Class: Chocomint::Orchestrator

Inherits:
Object
  • Object
show all
Defined in:
lib/chocomint/orchestrator.rb

Overview

ツール実行を完全制御するオーケストレータ (DESIGN §5, §6, §9)。 LLM は提案者と監査者に限定し、実行・判定・再試行・記録は Ruby が担う (DESIGN §20)。

Defined Under Namespace

Classes: Result, StepResult

Instance Method Summary collapse

Constructor Details

#initialize(primary:, registry:, machine_validator:, semantic_validator:, logger:, max_retry: 10, timeout_sec: 30) ⇒ Orchestrator

Returns a new instance of Orchestrator.



14
15
16
17
18
19
20
21
22
23
# File 'lib/chocomint/orchestrator.rb', line 14

def initialize(primary:, registry:, machine_validator:, semantic_validator:, logger:,
               max_retry: 10, timeout_sec: 30)
  @primary = primary
  @registry = registry
  @machine_validator = machine_validator
  @semantic_validator = semantic_validator
  @logger = logger
  @max_retry = max_retry
  @timeout_sec = timeout_sec
end

Instance Method Details

#run(request, expectations: nil) ⇒ Object

成功時は Result(status: "PASS")。回復不能時は例外を送出する (DESIGN §14)。



42
43
44
45
46
47
48
49
50
51
52
53
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
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
# File 'lib/chocomint/orchestrator.rb', line 42

def run(request, expectations: nil)
  trace_id = SecureRandom.uuid

  # Step 1: Tool Proposal — JSON invalid / unknown tool は FAIL。
  # LLM 通信エラー (タイムアウト等) は一時的な障害として RETRY に乗せる。
  proposal, propose_error = propose(request, trace_id)

  attempt = 0
  # 初回から前回までの全試行 (提案 + 失敗原因) を蓄積し、再提案時にまとめて渡す。
  history = []
  loop do
    attempt += 1
    # backoff は上限を設けて過度な待機を防ぐ (attempt 数増加に対応 / DESIGN §6)。
    sleep([attempt - 1, 5].min) if attempt > 1

    if proposal.nil?
      # propose/repropose が LLM 通信エラーで失敗した回。ツール実行はスキップし RETRY へ。
      result = { exit_code: 1, stdout: "", stderr: propose_error.to_s, duration_ms: 0 }
      status = "RETRY"
      verifier_output = nil
      error = propose_error
    else
      result, machine_ok, error = execute_and_machine_validate(proposal)

      if machine_ok
        begin
          pass, verifier_output = @semantic_validator.validate(
            request: request, result: result, expectations: expectations,
            trace_id: trace_id, proposal: proposal
          )
        rescue Chocomint::Error => e
          # verifier の LLM 通信エラーも一時的な障害として RETRY 扱いにする。
          pass = false
          verifier_output = nil
          error = "verifier request failed: #{e.message}"
        end

        if pass
          @logger.record(trace_id: trace_id, attempt: attempt, request: request,
                         proposal: proposal, result: result, status: "PASS",
                         verifier_output: verifier_output)
          return Result.new(status: "PASS", trace_id: trace_id, attempts: attempt,
                            result: result, proposal: proposal)
        end

        status = "RETRY" # verifier=0 or invalid
        error ||= "semantic validation failed: #{verifier_output}"
      else
        verifier_output = nil
        status = "RETRY"
        result ||= { exit_code: 1, stdout: "", stderr: error.to_s, duration_ms: 0 }
        # tool 例外時は error が入るが、ツールが正常応答で exit_code!=0 を
        # 返しただけの場合は error が nil のまま残るため、result のエラー内容で補う
        # (repropose の feedback に失敗理由を確実に載せるため)。
        error ||= result[:stderr]&.empty? == false ? result[:stderr] : "machine validation failed"
      end
    end

    @logger.record(trace_id: trace_id, attempt: attempt, request: request,
                   proposal: proposal || { "error" => error.to_s }, result: result,
                   status: status, verifier_output: verifier_output)

    # この試行の提案と失敗原因を履歴に残す (次の再提案の材料にするため nil でも保持する)。
    history << { attempt: attempt, proposal: proposal, error: error }

    if attempt < @max_retry
      # 初回〜前回までの全試行を渡し、同じ失敗を繰り返さないよう再提案する (DESIGN §6)。
      proposal, propose_error = repropose(request, history, trace_id)
      next
    end

    @logger.record(trace_id: trace_id, attempt: attempt, request: request,
                   proposal: proposal || { "error" => error.to_s }, result: result,
                   status: "FAIL", verifier_output: verifier_output)
    raise RetryLimitExceededError, "retry limit exceeded (#{@max_retry})"
  end
end

#run_step(proposal) ⇒ Object

マルチステップ (Planner) 用: 確定済みの 1 proposal を実行し、機械検証する。 LLM への再提案は行わず、実行 + machine 検証の結果だけを返す (提案・全体達成判定・ループ制御は Planner の責務)。 提案の形状 (未知ツール / スキーマ不一致) は事前に検証し、不正なら例外を送出する。 戻り値: StepResult(machine_ok, result, error)。



32
33
34
35
36
37
38
39
# File 'lib/chocomint/orchestrator.rb', line 32

def run_step(proposal)
  validate_proposal_shape!(proposal)
  result, machine_ok, error = execute_and_machine_validate(proposal)
  result ||= { exit_code: 1, stdout: "", stderr: error.to_s, duration_ms: 0 }
  # machine 検証は通ったが error が無い正常応答で exit_code!=0 のケースを補う。
  error ||= result[:stderr].to_s.empty? ? nil : result[:stderr] unless machine_ok
  StepResult.new(machine_ok: machine_ok, result: result, error: error)
end