crabmate 0.4.0

Rust AI agent: OpenAI-compatible chat/completions, function calling, HTTP serve, ops CLI
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
fn build_agent_config_from_finalize(mid: FinalizeAfterRoles, tail: FinalizeTailScalars) -> AgentConfig {
    assemble_agent_config_from_finalize(&mid, &tail)
}

fn assemble_agent_config_from_finalize(mid: &FinalizeAfterRoles, tail: &FinalizeTailScalars) -> AgentConfig {
    AgentConfig {
        llm: finalize_section_llm_connection(mid),
        session_ui: finalize_section_session_ui(mid),
        command_exec: finalize_section_command_exec(mid),
        llm_sampling: finalize_section_llm_sampling(mid),
        llm_vendor_flags: finalize_section_llm_vendor_flags(mid),
        llm_http_retry: finalize_section_llm_http_retry(mid),
        weather_tool: finalize_section_weather_tool(mid),
        web_search: finalize_section_web_search(tail),
        http_fetch: finalize_section_http_fetch(tail),
        per_plan_policy: finalize_section_per_plan_policy(mid, tail),
        roles_prompts: finalize_section_roles_prompts(mid),
        cursor_rules: finalize_section_cursor_rules(tail),
        skills: finalize_section_skills(tail),
        tool_transcript: finalize_section_tool_transcript(tail),
        agent_thinking_trace: finalize_section_agent_thinking_trace(tail),
        agent_tool_stats: finalize_section_agent_tool_stats(tail),
        thinking_echo: finalize_section_thinking_echo(tail),
        context_pipeline: finalize_section_context_pipeline(mid, tail),
        workspace_roots: finalize_section_workspace_roots(mid),
        web_api: finalize_section_web_api(tail),
        chat_queues_cache: finalize_section_chat_queues_cache(tail),
        session_workspace_changelist: finalize_section_session_workspace_changelist(tail),
        sync_tool_sandbox: finalize_section_sync_tool_sandbox(tail),
        conversation_persistence: finalize_section_conversation_persistence(mid, tail),
        context_bootstrap_inject: finalize_section_context_bootstrap_inject(tail),
        tool_call_explain: finalize_section_tool_call_explain(tail),
        long_term_memory: finalize_section_long_term_memory(mid),
        mcp_client: finalize_section_mcp_client(tail),
        codebase_semantic: finalize_section_codebase_semantic(mid),
        tool_registry_policy: finalize_section_tool_registry_policy(mid),
        turn_budget: finalize_section_turn_budget(),
    }
}

fn finalize_section_llm_connection(mid: &FinalizeAfterRoles) -> types::LlmConnectionConfig {
    types::LlmConnectionConfig {
        api_base: mid.b.llm.api_base.clone(),
        model: mid.b.llm.model.clone(),
        planner_model: mid.b.llm.planner_model.clone(),
        executor_model: mid.b.llm.executor_model.clone(),
        llm_http_auth_mode: mid.intent.llm_http_auth_mode,
    }
}

fn finalize_section_session_ui(mid: &FinalizeAfterRoles) -> types::SessionUiConfig {
    types::SessionUiConfig {
        max_message_history: mid.max_message_history,
    }
}

fn finalize_section_command_exec(mid: &FinalizeAfterRoles) -> types::CommandExecConfig {
    types::CommandExecConfig {
        command_timeout_secs: mid.command_timeout_secs,
        command_max_output_len: mid.command_max_output_len,
        allowed_commands: mid.allowed_commands.clone(),
        run_command_working_dir: mid.run_command_working_dir.display().to_string(),
        allow_external_path_with_approval: mid.allow_external_path_with_approval,
    }
}

fn finalize_section_llm_sampling(mid: &FinalizeAfterRoles) -> types::LlmSamplingConfig {
    types::LlmSamplingConfig {
        max_tokens: mid.max_tokens,
        llm_context_tokens: mid.llm_context_tokens,
        temperature: mid.temperature,
        llm_seed: mid.b.llm_sampling.llm_seed,
    }
}

fn finalize_section_llm_vendor_flags(mid: &FinalizeAfterRoles) -> types::LlmVendorFlagsConfig {
    types::LlmVendorFlagsConfig {
        llm_reasoning_split: mid.intent.llm_reasoning_split,
        llm_bigmodel_thinking: mid.b.llm_vendor.llm_bigmodel_thinking.unwrap_or(false),
        llm_kimi_thinking_disabled: mid.b.llm_vendor.llm_kimi_thinking_disabled.unwrap_or(false),
    }
}

fn finalize_section_llm_http_retry(mid: &FinalizeAfterRoles) -> types::LlmHttpRetryConfig {
    types::LlmHttpRetryConfig {
        api_timeout_secs: mid.api_timeout_secs,
        api_max_retries: mid.api_max_retries,
        api_retry_delay_secs: mid.api_retry_delay_secs,
    }
}

fn finalize_section_weather_tool(mid: &FinalizeAfterRoles) -> types::WeatherToolConfig {
    types::WeatherToolConfig {
        weather_timeout_secs: mid.weather_timeout_secs,
    }
}

fn finalize_section_web_search(tail: &FinalizeTailScalars) -> types::WebSearchConfigSection {
    types::WebSearchConfigSection {
        web_search_provider: tail.web_search_provider,
        web_search_api_key: tail.web_search_api_key.clone(),
        web_search_timeout_secs: tail.web_search_timeout_secs,
        web_search_max_results: tail.web_search_max_results,
    }
}

fn finalize_section_http_fetch(tail: &FinalizeTailScalars) -> types::HttpFetchConfigSection {
    types::HttpFetchConfigSection {
        http_fetch_allowed_prefixes: tail.http_fetch_allowed_prefixes.clone(),
        http_fetch_timeout_secs: tail.http_fetch_timeout_secs,
        http_fetch_max_response_bytes: tail.http_fetch_max_response_bytes,
    }
}

fn finalize_section_per_plan_policy(
    mid: &FinalizeAfterRoles,
    tail: &FinalizeTailScalars,
) -> types::PerPlanPolicyConfig {
    types::PerPlanPolicyConfig {
        reflection_default_max_rounds: mid.reflection_default_max_rounds,
        final_plan_requirement: tail.final_plan_requirement,
        plan_rewrite_max_attempts: tail.plan_rewrite_max_attempts,
        final_plan_require_strict_workflow_node_coverage: tail
            .final_plan_require_strict_workflow_node_coverage,
        final_plan_semantic_check_enabled: tail.final_plan_semantic_check_enabled,
        final_plan_semantic_check_accept_legacy_text: tail
            .final_plan_semantic_check_accept_legacy_text,
        final_plan_semantic_check_max_non_readonly_tools: tail
            .final_plan_semantic_check_max_non_readonly_tools,
        final_plan_semantic_check_max_tokens: tail.final_plan_semantic_check_max_tokens,
        planner_executor_mode: tail.planner_executor_mode,
        orchestration_profile: tail.orchestration_profile,
    }
}

fn finalize_section_roles_prompts(mid: &FinalizeAfterRoles) -> types::RolesPromptsConfig {
    types::RolesPromptsConfig {
        system_prompt: mid.system_prompt.clone(),
        default_agent_role_id: mid.default_agent_role_id.clone(),
        agent_roles: mid.agent_roles.clone(),
        coding_workbench_enabled: mid.coding_workbench_enabled,
        coding_workbench_increment_file: mid.coding_workbench_increment_file.clone(),
        default_session_mode: mid.default_session_mode,
    }
}

fn finalize_section_cursor_rules(tail: &FinalizeTailScalars) -> types::CursorRulesConfigSection {
    types::CursorRulesConfigSection {
        cursor_rules_enabled: tail.cursor_rules_enabled,
        cursor_rules_dir: tail.cursor_rules_dir.clone(),
        cursor_rules_include_agents_md: tail.cursor_rules_include_agents_md,
        cursor_rules_max_chars: tail.cursor_rules_max_chars as usize,
    }
}

fn finalize_section_skills(tail: &FinalizeTailScalars) -> types::SkillsConfigSection {
    types::SkillsConfigSection {
        skills_enabled: tail.skills_enabled,
        skills_dir: tail.skills_dir.clone(),
        skills_user_dir: tail.skills_user_dir.clone(),
        skills_system_dir: tail.skills_system_dir.clone(),
        skills_max_chars: tail.skills_max_chars as usize,
        skills_top_k: tail.skills_top_k,
    }
}

fn finalize_section_tool_transcript(tail: &FinalizeTailScalars) -> types::ToolTranscriptConfig {
    types::ToolTranscriptConfig {
        tool_message_max_chars: tail.tool_message_max_chars,
        tool_result_envelope_v1: tail.tool_result_envelope_v1,
        sse_tool_call_include_arguments: tail.sse_tool_call_include_arguments,
    }
}

fn finalize_section_agent_thinking_trace(tail: &FinalizeTailScalars) -> types::AgentThinkingTraceConfig {
    types::AgentThinkingTraceConfig {
        agent_thinking_trace_enabled: tail.agent_thinking_trace_enabled,
    }
}

fn finalize_section_agent_tool_stats(tail: &FinalizeTailScalars) -> types::AgentToolStatsConfig {
    types::AgentToolStatsConfig {
        agent_tool_stats_enabled: tail.agent_tool_stats_enabled,
        agent_tool_stats_window_events: tail.agent_tool_stats_window_events,
        agent_tool_stats_min_samples: tail.agent_tool_stats_min_samples,
        agent_tool_stats_max_chars: tail.agent_tool_stats_max_chars,
        agent_tool_stats_warn_below_success_ratio: tail.agent_tool_stats_warn_below_success_ratio,
    }
}

fn finalize_section_thinking_echo(tail: &FinalizeTailScalars) -> types::ThinkingEchoConfig {
    types::ThinkingEchoConfig {
        thinking_avoid_echo_system_prompt: tail.thinking_avoid_echo_system_prompt,
        thinking_avoid_echo_appendix: tail.thinking_avoid_echo_appendix.clone(),
    }
}

fn finalize_section_context_pipeline(
    mid: &FinalizeAfterRoles,
    tail: &FinalizeTailScalars,
) -> types::ContextPipelineConfig {
    let (context_summary_system, context_summary_user_template) = resolve_context_summary_prompts(
        mid.b.context_pipeline.context_summary_system_file.as_deref(),
        mid.b.context_pipeline.context_summary_user_file.as_deref(),
        &mid.system_prompt_search_bases,
        mid.run_command_working_dir.as_path(),
    );
    types::ContextPipelineConfig {
        context_char_budget: tail.context_char_budget,
        context_min_messages_after_system: tail.context_min_messages_after_system,
        context_summary_trigger_chars: tail.context_summary_trigger_chars,
        context_summary_tail_messages: tail.context_summary_tail_messages,
        context_summary_max_tokens: tail.context_summary_max_tokens,
        context_summary_transcript_max_chars: tail.context_summary_transcript_max_chars,
        context_summary_system,
        context_summary_user_template,
    }
}

fn finalize_section_workspace_roots(mid: &FinalizeAfterRoles) -> types::WorkspaceRootsConfig {
    types::WorkspaceRootsConfig {
        workspace_allowed_roots: mid.workspace_allowed_roots.clone(),
        web_workspace_pool: mid.web_workspace_pool.clone(),
    }
}

fn finalize_section_web_api(tail: &FinalizeTailScalars) -> types::WebApiConfig {
    types::WebApiConfig {
        web_api_bearer_token: tail.web_api_bearer_token.clone(),
        web_api_require_bearer: tail.web_api_require_bearer,
        web_cors_allowed_origins: tail.web_cors_allowed_origins.clone(),
        web_audit_log_write_tools: tail.web_audit_log_write_tools,
        web_audit_trust_x_forwarded_for: tail.web_audit_trust_x_forwarded_for,
        allow_insecure_no_auth_for_non_loopback: tail.allow_insecure_no_auth_for_non_loopback,
        health_llm_models_probe: tail.health_llm_models_probe,
        health_llm_models_probe_cache_secs: tail.health_llm_models_probe_cache_secs,
    }
}

fn finalize_section_chat_queues_cache(tail: &FinalizeTailScalars) -> types::ChatQueuesCacheConfig {
    types::ChatQueuesCacheConfig {
        chat_queue_max_concurrent: tail.chat_queue_max_concurrent,
        chat_queue_max_pending: tail.chat_queue_max_pending,
        parallel_readonly_tools_max: tail.parallel_readonly_tools_max,
        read_file_turn_cache_max_entries: tail.read_file_turn_cache_max_entries,
        readonly_tool_ttl_cache_secs: tail.readonly_tool_ttl_cache_secs,
        readonly_tool_ttl_cache_max_entries: tail.readonly_tool_ttl_cache_max_entries,
        test_result_cache_enabled: tail.test_result_cache_enabled,
        test_result_cache_max_entries: tail.test_result_cache_max_entries,
    }
}

fn finalize_section_session_workspace_changelist(
    tail: &FinalizeTailScalars,
) -> types::SessionWorkspaceChangelistConfig {
    types::SessionWorkspaceChangelistConfig {
        session_workspace_changelist_enabled: tail.session_workspace_changelist_enabled,
        session_workspace_changelist_max_chars: tail.session_workspace_changelist_max_chars,
    }
}

fn finalize_section_sync_tool_sandbox(tail: &FinalizeTailScalars) -> types::SyncToolSandboxConfig {
    types::SyncToolSandboxConfig {
        sync_default_tool_sandbox_mode: tail.sync_default_tool_sandbox_mode,
        sync_default_tool_sandbox_docker_image: tail.sync_default_tool_sandbox_docker_image.clone(),
        sync_default_tool_sandbox_docker_network: tail.sync_default_tool_sandbox_docker_network.clone(),
        sync_default_tool_sandbox_docker_timeout_secs: tail.sync_default_tool_sandbox_docker_timeout_secs,
        sync_default_tool_sandbox_docker_user: tail.sync_default_tool_sandbox_docker_user.clone(),
    }
}

fn finalize_section_conversation_persistence(
    mid: &FinalizeAfterRoles,
    tail: &FinalizeTailScalars,
) -> types::ConversationPersistenceConfig {
    types::ConversationPersistenceConfig {
        conversation_store_sqlite_path: tail.conversation_store_sqlite_path.clone(),
        scheduled_agent_tasks: mid.scheduled_agent_tasks.clone(),
    }
}

fn finalize_section_context_bootstrap_inject(tail: &FinalizeTailScalars) -> types::ContextBootstrapInjectConfig {
    types::ContextBootstrapInjectConfig {
        agent_memory_file_enabled: tail.agent_memory_file_enabled,
        agent_memory_file: tail.agent_memory_file.clone(),
        agent_memory_file_max_chars: tail.agent_memory_file_max_chars,
        living_docs_inject_enabled: tail.living_docs_inject_enabled,
        living_docs_relative_dir: tail.living_docs_relative_dir.clone(),
        living_docs_inject_max_chars: tail.living_docs_inject_max_chars,
        living_docs_file_max_each_chars: tail.living_docs_file_max_each_chars,
        project_profile_inject_enabled: tail.project_profile_inject_enabled,
        project_profile_inject_max_chars: tail.project_profile_inject_max_chars,
        project_dependency_brief_inject_enabled: tail.project_dependency_brief_inject_enabled,
        project_dependency_brief_inject_max_chars: tail.project_dependency_brief_inject_max_chars,
    }
}

fn finalize_section_tool_call_explain(tail: &FinalizeTailScalars) -> types::ToolCallExplainConfig {
    types::ToolCallExplainConfig {
        tool_call_explain_enabled: tail.tool_call_explain_enabled,
        tool_call_explain_min_chars: tail.tool_call_explain_min_chars,
        tool_call_explain_max_chars: tail.tool_call_explain_max_chars,
    }
}

fn finalize_section_long_term_memory(mid: &FinalizeAfterRoles) -> types::LongTermMemoryConfig {
    let ltm = &mid.ltm;
    types::LongTermMemoryConfig {
        long_term_memory_enabled: ltm.long_term_memory_enabled,
        long_term_memory_scope_mode: ltm.long_term_memory_scope_mode,
        long_term_memory_vector_backend: ltm.long_term_memory_vector_backend,
        long_term_memory_max_entries: ltm.long_term_memory_max_entries,
        long_term_memory_inject_max_chars: ltm.long_term_memory_inject_max_chars,
        long_term_memory_store_sqlite_path: ltm.long_term_memory_store_sqlite_path.clone(),
        long_term_memory_top_k: ltm.long_term_memory_top_k,
        long_term_memory_max_chars_per_chunk: ltm.long_term_memory_max_chars_per_chunk,
        long_term_memory_min_chars_to_index: ltm.long_term_memory_min_chars_to_index,
        long_term_memory_async_index: ltm.long_term_memory_async_index,
        long_term_memory_auto_index_turns: ltm.long_term_memory_auto_index_turns,
        long_term_memory_auto_summarize_experience: ltm
            .long_term_memory_auto_summarize_experience,
        long_term_memory_prioritize_experience_recall: ltm
            .long_term_memory_prioritize_experience_recall,
        long_term_memory_default_ttl_secs: ltm.long_term_memory_default_ttl_secs,
    }
}

fn finalize_section_mcp_client(tail: &FinalizeTailScalars) -> types::McpClientConfig {
    types::McpClientConfig {
        mcp_enabled: tail.mcp_enabled,
        mcp_command: tail.mcp_command.clone(),
        mcp_tool_timeout_secs: tail.mcp_tool_timeout_secs,
    }
}

fn finalize_section_codebase_semantic(mid: &FinalizeAfterRoles) -> types::CodebaseSemanticConfig {
    let sem = &mid.sem;
    types::CodebaseSemanticConfig {
        codebase_semantic_search_enabled: sem.codebase_semantic_search_enabled,
        codebase_semantic_invalidate_on_workspace_change: sem.codebase_semantic_invalidate_on_workspace_change,
        codebase_semantic_index_sqlite_path: sem.codebase_semantic_index_sqlite_path.clone(),
        codebase_semantic_max_file_bytes: sem.codebase_semantic_max_file_bytes,
        codebase_semantic_chunk_max_chars: sem.codebase_semantic_chunk_max_chars,
        codebase_semantic_top_k: sem.codebase_semantic_top_k,
        codebase_semantic_query_max_chunks: sem.codebase_semantic_query_max_chunks,
        codebase_semantic_rebuild_max_files: sem.codebase_semantic_rebuild_max_files,
        codebase_semantic_rebuild_incremental: sem.codebase_semantic_rebuild_incremental,
        codebase_semantic_hybrid_alpha: sem.codebase_semantic_hybrid_alpha,
        codebase_semantic_fts_top_n: sem.codebase_semantic_fts_top_n,
        codebase_semantic_hybrid_semantic_pool: sem.codebase_semantic_hybrid_semantic_pool,
    }
}

fn finalize_section_tool_registry_policy(mid: &FinalizeAfterRoles) -> types::ToolRegistryPolicyConfig {
    let tr = &mid.tr;
    types::ToolRegistryPolicyConfig {
        tool_registry_http_fetch_wall_timeout_secs: tr.tool_registry_http_fetch_wall_timeout_secs,
        tool_registry_http_request_wall_timeout_secs: tr.tool_registry_http_request_wall_timeout_secs,
        tool_registry_parallel_wall_timeout_secs: tr.tool_registry_parallel_wall_timeout_secs.clone(),
        tool_registry_parallel_sync_denied_tools: tr.tool_registry_parallel_sync_denied_tools.clone(),
        tool_registry_parallel_sync_denied_prefixes: tr.tool_registry_parallel_sync_denied_prefixes.clone(),
        tool_registry_sync_default_inline_tools: tr.tool_registry_sync_default_inline_tools.clone(),
        tool_registry_write_effect_tools: tr.tool_registry_write_effect_tools.clone(),
        tool_registry_sub_agent_patch_write_extra_tools: tr.tool_registry_sub_agent_patch_write_extra_tools.clone(),
        tool_registry_sub_agent_test_runner_extra_tools: tr.tool_registry_sub_agent_test_runner_extra_tools.clone(),
        tool_registry_sub_agent_review_readonly_deny_tools: tr.tool_registry_sub_agent_review_readonly_deny_tools.clone(),
    }
}

fn finalize_section_turn_budget() -> types::TurnBudgetConfig {
    let mut cfg = types::TurnBudgetConfig {
        max_turn_duration_seconds: 600,
        max_turn_tokens: 0,
        max_llm_calls_per_turn: 0,
        max_outer_loop_iterations: 0,
        budget_degradation_enabled: false,
        budget_degradation_threshold_percent: 80,
    };
    if let Ok(v) = std::env::var("CM_MAX_TURN_DURATION_SECONDS")
        && let Ok(n) = v.trim().parse::<u64>()
    {
        cfg.max_turn_duration_seconds = n;
    }
    if let Ok(v) = std::env::var("CM_MAX_TURN_TOKENS")
        && let Ok(n) = v.trim().parse::<usize>()
    {
        cfg.max_turn_tokens = n;
    }
    if let Ok(v) = std::env::var("CM_MAX_LLM_CALLS_PER_TURN")
        && let Ok(n) = v.trim().parse::<u32>()
    {
        cfg.max_llm_calls_per_turn = n;
    }
    if let Ok(v) = std::env::var("CM_TURN_BUDGET_DEGRADATION_ENABLED")
        && let Some(b) = crate::cm_config::source::parse_bool_like(&v)
    {
        cfg.budget_degradation_enabled = b;
    }
    if let Ok(v) = std::env::var("CM_TURN_BUDGET_DEGRADATION_THRESHOLD_PERCENT")
        && let Ok(n) = v.trim().parse::<u8>()
    {
        cfg.budget_degradation_threshold_percent = n.clamp(50, 99);
    }
    cfg
}

fn finalize_agent_config_tail(mid: FinalizeAfterRoles) -> Result<AgentConfig, String> {
    let tail = derive_finalize_tail_scalars(&mid)?;
    Ok(build_agent_config_from_finalize(mid, tail))
}

pub(super) fn finalize(
    b: ConfigBuilder,
    system_prompt_search_bases: Vec<PathBuf>,
) -> Result<AgentConfig, String> {
    finalize_agent_config(b, system_prompt_search_bases)
}