Class: AgentHarness::Providers::Kilocode

Inherits:
Base
  • Object
show all
Defined in:
lib/agent_harness/providers/kilocode.rb

Overview

Kilocode CLI provider

Provides integration with the Kilocode CLI tool.

Constant Summary collapse

PACKAGE_NAME =
"@kilocode/cli"
DEFAULT_VERSION =
"7.4.16"
SUPPORTED_VERSION_REQUIREMENT =
"= #{DEFAULT_VERSION}"
STRUCTURED_EVENT_TYPES =
%w[text error step_finish result usage].freeze
MODEL_NAMES =
%w[
  glm-5
  glm-5.1
  glm-5.1-free
  glm-5.1-thinking
  glm-5.2
  glm-5.2-fast
  glm-5.2-flex
  glm-5.2-free
  glm-5.2-nitro
  glm-5.2-short
  glm-5.2-short-fast
  glm-5.2-short-fast-flex
  glm-5.2-short-flex
  glm-5p1
  glm-5p1-fast
  glm-5p2
  glm-5p2-fast
  glm-5v-turbo
].freeze
MODEL_CATALOG =
MODEL_NAMES.map do |name|
  tier = name.include?("free") ? "free" : "standard"
  tier = "advanced" if %w[glm-5.1 glm-5.1-thinking glm-5.2 glm-5p1 glm-5p2].include?(name)

  {name: name, family: name, tier: tier, provider: "kilocode"}.freeze
end.freeze
MODEL_FAMILY_PATTERN =
/\Aglm-5(?:[a-z][\w.-]*|[.-][\w.-]+)?\z/i
DEFAULT_PERMISSION_EXTERNAL_DIRECTORY_PATTERNS =

Kilo CLI (an OpenCode fork) ships the same external_directory permission category as OpenCode, defaulting to "ask" for anything outside the project dir. In non-interactive execution there is no human to answer the prompt, so dedicated read/write/edit tool calls targeting paths outside the project dir are auto-rejected and the agent silently loses access to its own scratch output or its own config/data files, killing the run without a recoverable error.

This default rule broadens the allowlist to all of /tmp (so the agent can read back files it or its sub-agents wrote there) and to the full agent home directory (so the agent can inspect/maintain its own config, cache, and data files such as ~/.config/kilocode, ~/.local/share/kilo, ~/.cache). The container is already isolated (Docker, non-root user) and the agent runs with --auto which approves everything inside the project dir. See #289 (precedent: #282/#277).

["/tmp/**", "/home/agent/**"].freeze
DEFAULT_PERMISSION_CONFIG =
{
  "external_directory" => DEFAULT_PERMISSION_EXTERNAL_DIRECTORY_PATTERNS
    .to_h { |pattern| [pattern, "allow"] }
    .freeze
}.freeze
USAGE_EVENT_TYPES =
%w[result usage].freeze
TOKEN_USAGE_KEYS =
%w[
  input_tokens
  output_tokens
  total_tokens
  total
  reasoning_tokens
  cache_creation_input_tokens
  cache_read_input_tokens
  cache_write_input_tokens
].freeze

Constants inherited from Base

Base::COMMON_ERROR_PATTERNS, Base::DEFAULT_SMOKE_TEST_CONTRACT

Instance Attribute Summary

Attributes inherited from Base

#config, #executor, #logger

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#api_key_env_var_names, #api_key_unset_vars, #check_quota, #cli_env_overrides, #configure, #initialize, #parse_container_output, #parse_test_error, #plan_execution, #preflight_check, #sandboxed_environment?, #send_chat_message, #send_message, #subscription_unset_vars, #update_quota_from_headers

Methods included from RateLimitResetParsing

#parse_rate_limit_reset

Methods included from Adapter

#auth_lock_config, #auth_type, #build_mcp_flags, #chat_transport, #chat_transport_type, #configuration_schema, #dangerous_mode_flags, #error_classification_patterns, #fetch_mcp_servers, #health_status, included, metadata_package_name, #noisy_error_patterns, normalize_metadata_installation, normalize_metadata_runtime_requirements, normalize_metadata_source_type, normalize_metadata_version_requirement, #notify_hook_content, #parse_rate_limit_reset, #plan_execution, #preflight_check, #send_message, #session_flags, #smoke_test, #smoke_test_contract, #supported_mcp_transports, #supports_chat?, #supports_dangerous_mode?, #supports_mcp?, #supports_message_tool_injection?, #supports_sessions?, #supports_text_mode?, #supports_token_counting?, #supports_tool_control?, #token_usage_from_api_response, #translate_error, #validate_config, #validate_mcp_servers!

Constructor Details

This class inherits a constructor from AgentHarness::Providers::Base

Class Method Details

.available?Boolean

Returns:

  • (Boolean)


85
86
87
88
# File 'lib/agent_harness/providers/kilocode.rb', line 85

def available?
  executor = AgentHarness.configuration.command_executor
  !!executor.which(binary_name)
end

.binary_nameObject



81
82
83
# File 'lib/agent_harness/providers/kilocode.rb', line 81

def binary_name
  "kilo"
end

.discover_modelsObject



101
102
103
104
105
# File 'lib/agent_harness/providers/kilocode.rb', line 101

def discover_models
  return [] unless available?

  MODEL_CATALOG.map(&:dup)
end

.firewall_requirementsObject



90
91
92
93
94
95
# File 'lib/agent_harness/providers/kilocode.rb', line 90

def firewall_requirements
  {
    domains: [],
    ip_ranges: []
  }
end

.install_command(version: DEFAULT_VERSION) ⇒ Object



136
137
138
# File 'lib/agent_harness/providers/kilocode.rb', line 136

def install_command(version: DEFAULT_VERSION)
  installation_contract(version: version)[:install_command]
end

.installation_contract(version: DEFAULT_VERSION) ⇒ Object



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/agent_harness/providers/kilocode.rb', line 119

def installation_contract(version: DEFAULT_VERSION)
  version = version.strip if version.respond_to?(:strip)
  validate_install_version!(version)
  package_spec = "#{PACKAGE_NAME}@#{version}"

  {
    source: {
      type: :npm,
      package: PACKAGE_NAME
    },
    install_command: ["npm", "install", "-g", "--ignore-scripts", package_spec],
    binary_name: binary_name,
    default_version: DEFAULT_VERSION,
    supported_version_requirement: SUPPORTED_VERSION_REQUIREMENT
  }
end

.instruction_file_pathsObject



97
98
99
# File 'lib/agent_harness/providers/kilocode.rb', line 97

def instruction_file_paths
  []
end

.model_family(provider_model_name) ⇒ Object



107
108
109
# File 'lib/agent_harness/providers/kilocode.rb', line 107

def model_family(provider_model_name)
  provider_model_name.to_s
end

.provider_model_name(family_name) ⇒ Object



111
112
113
# File 'lib/agent_harness/providers/kilocode.rb', line 111

def provider_model_name(family_name)
  family_name.to_s
end

.provider_nameObject



77
78
79
# File 'lib/agent_harness/providers/kilocode.rb', line 77

def provider_name
  :kilocode
end

.smoke_test_contractObject



140
141
142
# File 'lib/agent_harness/providers/kilocode.rb', line 140

def smoke_test_contract
  Base::DEFAULT_SMOKE_TEST_CONTRACT
end

.supports_model_family?(family_name) ⇒ Boolean

Returns:

  • (Boolean)


115
116
117
# File 'lib/agent_harness/providers/kilocode.rb', line 115

def supports_model_family?(family_name)
  MODEL_FAMILY_PATTERN.match?(family_name.to_s)
end

Instance Method Details

#capabilitiesObject



182
183
184
185
186
187
188
189
190
191
192
# File 'lib/agent_harness/providers/kilocode.rb', line 182

def capabilities
  {
    streaming: false,
    file_upload: false,
    vision: false,
    tool_use: false,
    json_mode: false,
    mcp: false,
    dangerous_mode: false
  }
end

#config_file_content(options = {}) ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/agent_harness/providers/kilocode.rb', line 227

def config_file_content(options = {})
  # Only use explicit provider_name or default to "openai".
  # api_provider is a generic backend label (e.g. "openrouter") that is not
  # a valid Kilo built-in provider ID, so we must not fall back to it here.
  provider_name = options[:provider_name] || "openai"
  model_id = options[:model_id]

  config = {"provider" => {provider_name => {}}}
  config["model"] = "#{provider_name}/#{model_id}" if model_id
  apply_default_external_directory_permission(config, options)

  config.to_json
end

#display_nameObject



174
175
176
# File 'lib/agent_harness/providers/kilocode.rb', line 174

def display_name
  "Kilocode CLI"
end

#error_patternsObject



241
242
243
# File 'lib/agent_harness/providers/kilocode.rb', line 241

def error_patterns
  COMMON_ERROR_PATTERNS
end

#execution_semanticsObject



245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/agent_harness/providers/kilocode.rb', line 245

def execution_semantics
  {
    prompt_delivery: :arg,
    output_format: :json,
    sandbox_aware: false,
    uses_subcommand: true,
    non_interactive_flag: nil,
    legitimate_exit_codes: [0],
    stderr_is_diagnostic: true,
    parses_rate_limit_reset: true
  }
end

#heartbeat_integration(heartbeat_file_path:) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/agent_harness/providers/kilocode.rb', line 198

def heartbeat_integration(heartbeat_file_path:)
  unless heartbeat_file_path.is_a?(String) && !heartbeat_file_path.strip.empty?
    raise ArgumentError, "heartbeat_file_path must be a non-empty String"
  end
  unless heartbeat_file_path.start_with?("/")
    raise ArgumentError, "heartbeat_file_path must be an absolute path (got #{heartbeat_file_path.inspect})"
  end

  hook_script = heartbeat_hook_script(heartbeat_file_path)
  config_payload = merge_heartbeat_hooks(hook_script)

  preparation = ExecutionPreparation.new(
    file_writes: [
      {
        path: heartbeat_hook_config_path,
        content: JSON.pretty_generate(config_payload),
        mode: 0o600
      }
    ]
  )

  {
    supported: true,
    env: {"KILO_HEARTBEAT_FILE" => heartbeat_file_path},
    preparation: preparation,
    granularity: :tool_call
  }
end

#nameObject



170
171
172
# File 'lib/agent_harness/providers/kilocode.rb', line 170

def name
  "kilocode"
end

#supports_activity_heartbeat?Boolean

Returns:

  • (Boolean)


194
195
196
# File 'lib/agent_harness/providers/kilocode.rb', line 194

def supports_activity_heartbeat?
  true
end

#test_command_overridesObject



178
179
180
# File 'lib/agent_harness/providers/kilocode.rb', line 178

def test_command_overrides
  ["--auto", "--print-logs"]
end