a3s-code-core 9.0.0

A3S Code Core - Embeddable AI agent library with tool execution
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
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
//! Native Rust implementations of all built-in tools
//!
//! These replace the previous `a3s-tools` binary backend with direct Rust
//! implementations that execute in-process. Each tool implements the `Tool` trait.

pub mod ask_user;
pub(crate) mod bash;
mod batch;
mod bm25;
mod code_intelligence;
mod download;
mod edit;
mod generate_object;
pub(crate) mod git;
mod glob_tool;
mod grep;
mod hybrid_search;
mod ls;
mod patch;
mod read;
mod safe_http;
mod search;
mod semantic_search;
mod update_plan;
mod web_fetch;
mod web_search;
mod write;

use super::registry::ToolRegistry;
use std::sync::Arc;

/// Normalize a source URL before it can enter durable tool/task metadata.
/// Credentials, query strings, and fragments are intentionally excluded.
pub(crate) fn safe_http_source_url(value: &str) -> Option<String> {
    let mut url = reqwest::Url::parse(value.trim()).ok()?;
    if !matches!(url.scheme(), "http" | "https") || url.host_str()?.is_empty() {
        return None;
    }
    url.set_username("").ok()?;
    url.set_password(None).ok()?;
    url.set_query(None);
    url.set_fragment(None);
    Some(url.to_string())
}

/// Register all baseline built-in tools with the registry, gated by
/// workspace capabilities.
///
/// Tools whose required capability is missing are not registered, so the model
/// never sees a tool the backend cannot service. `web_fetch` and `web_search`
/// have no workspace capability and are always registered.
///
/// Note: `batch` is NOT registered here — it requires an `Arc<ToolRegistry>`
/// and must be registered after the registry is wrapped in an Arc.
pub fn register_builtins(
    registry: &ToolRegistry,
    workspace_services: &crate::workspace::WorkspaceServices,
) {
    let capabilities = workspace_services.capabilities();
    if capabilities.read {
        registry.register_builtin(Arc::new(read::ReadTool));
        registry.register_builtin(Arc::new(ls::LsTool));
    }
    if capabilities.write {
        registry.register_builtin(Arc::new(write::WriteTool));
        if workspace_services.local_root().is_some() {
            registry.register_builtin(Arc::new(download::DownloadTool));
        }
    }
    if capabilities.read && capabilities.write {
        registry.register_builtin(Arc::new(edit::EditTool));
        registry.register_builtin(Arc::new(patch::PatchTool));
    }
    if capabilities.exec {
        registry.register_builtin(Arc::new(bash::BashTool));
    }
    let semantic_enabled = capabilities.read && workspace_services.workspace_retrieval().is_some();
    // Prefer durable FTS at call time when this workspace can host one. Do not
    // open native indexes during tool registration (Loading / session build).
    let persistent_backend_enabled = capabilities.read && workspace_services.local_root().is_some();
    if capabilities.search || semantic_enabled || persistent_backend_enabled {
        let search = search::SearchTool::new(capabilities.read)
            .with_backend_search(capabilities.search)
            .with_semantic(semantic_enabled)
            .with_persistent_backend(persistent_backend_enabled);
        registry.register_builtin(Arc::new(search));
    }
    if workspace_services.code_intelligence().is_some() {
        code_intelligence::register(registry);
    }
    if capabilities.git {
        registry.register_builtin(Arc::new(git::GitTool));
    }
    registry.register_builtin(Arc::new(web_fetch::WebFetchTool));
    registry.register_builtin(Arc::new(web_search::WebSearchTool::new()));
    // Session checklist updates (no workspace capability gate).
    registry.register_builtin(Arc::new(update_plan::UpdatePlanTool));
    registry.register_builtin(Arc::new(ask_user::AskUserTool));
}

#[cfg(test)]
pub(crate) fn repository_tool_parameter_schemas() -> Vec<(String, serde_json::Value)> {
    use crate::tools::Tool;

    let read = read::ReadTool;
    let search = search::SearchTool::new(true);
    let edit = edit::EditTool;
    vec![
        (read.name().to_string(), read.parameters()),
        (search.name().to_string(), search.parameters()),
        (edit.name().to_string(), edit.parameters()),
    ]
}

/// Register the batch tool. Must be called after the registry is wrapped in Arc.
pub fn register_batch(registry: &Arc<ToolRegistry>) {
    registry.register_builtin(Arc::new(batch::BatchTool::new_registry_bound(Arc::clone(
        registry,
    ))));
}

/// Register the programmatic tool calling wrapper.
pub fn register_program(registry: &Arc<ToolRegistry>) {
    register_program_with_catalog(
        registry,
        crate::program::ProgramCatalog::with_builtin_programs(),
    );
}

/// Register the programmatic tool calling wrapper with a custom catalog.
pub fn register_program_with_catalog(
    registry: &Arc<ToolRegistry>,
    catalog: crate::program::ProgramCatalog,
) {
    registry.register_builtin(Arc::new(
        crate::tools::ProgramTool::with_catalog_registry_bound(Arc::clone(registry), catalog),
    ));
}

/// Register the canonical `task` tool (multi-item fan-out included).
///
/// Model-visible `parallel_task` is not registered (`HARNESS-CONV4`).
/// Must be called after the registry is wrapped in Arc. Requires an LLM client
/// and the workspace path so child agent loops can be spawned inline.
/// Optionally accepts an MCP manager so child sessions inherit MCP tools.
pub fn register_task(
    registry: &Arc<ToolRegistry>,
    llm_client: Arc<dyn crate::llm::LlmClient>,
    agent_registry: Arc<crate::subagent::AgentRegistry>,
    workspace: String,
) {
    register_task_with_mcp(
        registry,
        llm_client,
        agent_registry,
        workspace,
        None,
        None,
        None,
    );
}

/// Register the task delegation tools with optional MCP manager and parent context.
///
/// When `mcp_manager` is provided, delegated child sessions will have access
/// to all MCP tools from connected servers.
/// When `parent_context` is provided, child runs inherit parent capabilities.
/// When `subagent_tracker` is provided, each task registers a
/// `CancellationToken` against it so callers can cancel by `task_id`.
pub fn register_task_with_mcp(
    registry: &Arc<ToolRegistry>,
    llm_client: Arc<dyn crate::llm::LlmClient>,
    agent_registry: Arc<crate::subagent::AgentRegistry>,
    workspace: String,
    mcp_manager: Option<Arc<crate::mcp::manager::McpManager>>,
    parent_context: Option<crate::child_run::ChildRunContext>,
    subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
) {
    register_task_with_mcp_managers(
        registry,
        llm_client,
        agent_registry,
        workspace,
        mcp_manager.into_iter().collect(),
        parent_context,
        subagent_tracker,
    );
}

/// Register task delegation tools with ordered MCP capability sources.
///
/// Each manager keeps ownership of its own connections. Later sources shadow
/// earlier sources on identical fully-qualified tool names inside child runs.
pub fn register_task_with_mcp_managers(
    registry: &Arc<ToolRegistry>,
    llm_client: Arc<dyn crate::llm::LlmClient>,
    agent_registry: Arc<crate::subagent::AgentRegistry>,
    workspace: String,
    mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
    parent_context: Option<crate::child_run::ChildRunContext>,
    subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
) {
    register_task_internal(
        registry,
        llm_client,
        agent_registry,
        workspace,
        mcp_managers,
        Vec::new(),
        parent_context,
        subagent_tracker,
        None,
    );
}

/// Register session task tools with the owning agent's shared scheduler.
#[allow(clippy::too_many_arguments)]
pub(crate) fn register_task_with_mcp_managers_and_scheduler(
    registry: &Arc<ToolRegistry>,
    llm_client: Arc<dyn crate::llm::LlmClient>,
    agent_registry: Arc<crate::subagent::AgentRegistry>,
    workspace: String,
    mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
    parent_context: Option<crate::child_run::ChildRunContext>,
    subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
    task_scheduler: Arc<crate::task_scheduler::TaskScheduler>,
) {
    register_task_with_mcp_sources_and_scheduler(
        registry,
        llm_client,
        agent_registry,
        workspace,
        mcp_managers,
        Vec::new(),
        parent_context,
        subagent_tracker,
        task_scheduler,
    );
}

/// Register Run-frozen task tools with compatibility managers and exact MCP
/// capability bindings.
#[allow(clippy::too_many_arguments)]
pub(crate) fn register_task_with_mcp_sources_and_scheduler(
    registry: &Arc<ToolRegistry>,
    llm_client: Arc<dyn crate::llm::LlmClient>,
    agent_registry: Arc<crate::subagent::AgentRegistry>,
    workspace: String,
    mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
    mcp_bindings: Vec<Arc<crate::mcp::McpBinding>>,
    parent_context: Option<crate::child_run::ChildRunContext>,
    subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
    task_scheduler: Arc<crate::task_scheduler::TaskScheduler>,
) {
    register_task_internal(
        registry,
        llm_client,
        agent_registry,
        workspace,
        mcp_managers,
        mcp_bindings,
        parent_context,
        subagent_tracker,
        Some(task_scheduler),
    );
}

#[allow(clippy::too_many_arguments)]
fn register_task_internal(
    registry: &Arc<ToolRegistry>,
    llm_client: Arc<dyn crate::llm::LlmClient>,
    agent_registry: Arc<crate::subagent::AgentRegistry>,
    workspace: String,
    mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
    mcp_bindings: Vec<Arc<crate::mcp::McpBinding>>,
    parent_context: Option<crate::child_run::ChildRunContext>,
    subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
    task_scheduler: Option<Arc<crate::task_scheduler::TaskScheduler>>,
) {
    use crate::tools::task::{TaskExecutor, TaskTool};
    let mut executor =
        TaskExecutor::with_mcp_managers(agent_registry, llm_client, workspace, mcp_managers)
            .with_projected_mcp_bindings(mcp_bindings);
    if let Some(ctx) = parent_context {
        executor = executor.with_parent_context(ctx);
    }
    if let Some(tracker) = subagent_tracker {
        executor = executor.with_subagent_tracker(tracker);
    }
    if let Some(task_scheduler) = task_scheduler {
        // Model-visible task tools run inside an already-admitted parent turn.
        // Only their detached background branch needs another global slot.
        executor = executor.with_task_scheduler(task_scheduler, false);
    }
    let executor = Arc::new(executor);
    registry.register_builtin(Arc::new(TaskTool::new(Arc::clone(&executor))));
    // `parallel_task` is removed from the model-visible registry (`HARNESS-CONV4`).
    // Fan-out uses the unified `task` tool; the historical ParallelTaskTool type
    // stays crate-internal for unit tests only.
}

/// Register the Skill tool for skill-based tool access control.
pub(crate) fn register_skill(
    registry: &Arc<ToolRegistry>,
    llm_client: Arc<dyn crate::llm::LlmClient>,
    skill_registry: Arc<crate::skills::SkillRegistry>,
    tool_executor: Arc<crate::tools::ToolExecutor>,
    base_config: crate::agent::AgentConfig,
) {
    use crate::tools::skill::{SearchSkillsTool, SkillTool};
    registry.register_builtin(Arc::new(SearchSkillsTool::new(Arc::clone(&skill_registry))));
    registry.register_builtin(Arc::new(SkillTool::new_registry_bound(
        skill_registry,
        llm_client,
        tool_executor,
        base_config,
    )));
}

/// Register the `generate_object` tool for structured JSON output.
///
/// Must be called after the registry is wrapped in Arc. Requires an LLM client
/// so the tool can make its own LLM calls for object generation.
pub fn register_generate_object(
    registry: &Arc<ToolRegistry>,
    llm_client: Arc<dyn crate::llm::LlmClient>,
) {
    registry.register_builtin(Arc::new(generate_object::GenerateObjectTool::new(
        llm_client,
    )));
}

#[cfg(test)]
mod tests {
    use super::register_builtins;
    use super::safe_http_source_url;
    use crate::tools::registry::ToolRegistry;
    use crate::workspace::{
        ChunkCatalogLimits, ChunkingConfig, ManifestWorkspaceBackend, WorkspaceChunkingStrategy,
        WorkspaceServices,
    };
    use std::sync::Arc;

    #[test]
    fn safe_source_url_removes_credentials_query_and_fragment() {
        assert_eq!(
            safe_http_source_url(
                "HTTPS://user:password@Example.COM/report?access_token=secret#section"
            )
            .as_deref(),
            Some("https://example.com/report")
        );
        assert!(safe_http_source_url("file:///tmp/source").is_none());
    }

    #[tokio::test]
    async fn register_builtins_does_not_open_durable_a3s_vec() {
        let temp = tempfile::tempdir().unwrap();
        let backend = ManifestWorkspaceBackend::new(temp.path());
        backend
            .configure_chunk_catalog(
                WorkspaceChunkingStrategy::Lines,
                ChunkingConfig::default(),
                ChunkCatalogLimits::default(),
            )
            .unwrap();
        let services = WorkspaceServices::local_with_retrieval_backend(Arc::clone(&backend));
        let registry = ToolRegistry::new(temp.path().to_path_buf());
        register_builtins(&registry, &services);
        assert!(
            backend.persistent_index().is_none(),
            "tool registration must not attach durable FTS"
        );
        assert!(
            !temp.path().join(".a3s-code").join("index").exists(),
            "tool registration must not create the durable index directory"
        );
        assert!(
            registry.get("search").is_some(),
            "local workspaces still register search for demand-driven durable FTS"
        );
    }

    #[test]
    fn register_builtins_hides_disabled_capabilities() {
        use crate::workspace::{
            LocalWorkspaceBackend, WorkspaceCapabilities, WorkspaceFileSystem, WorkspaceRef,
        };

        let temp = tempfile::tempdir().unwrap();
        let backend = Arc::new(LocalWorkspaceBackend::new(temp.path().to_path_buf()));
        let fs: Arc<dyn WorkspaceFileSystem> = backend;
        let services = WorkspaceServices::builder(
            WorkspaceRef::new("ws", temp.path().display().to_string()),
            fs,
        )
        .capabilities(WorkspaceCapabilities {
            read: true,
            write: false,
            exec: false,
            search: false,
            git: false,
            code_intelligence: false,
        })
        .build();
        let registry = ToolRegistry::new(temp.path().to_path_buf());
        register_builtins(&registry, &services);

        for present in [
            "read",
            "ls",
            "web_fetch",
            "web_search",
            "update_plan",
            "ask_user",
        ] {
            assert!(registry.contains(present), "{present} must stay registered");
        }
        for absent in [
            "write",
            "edit",
            "patch",
            "bash",
            "git",
            "download",
            "code_symbols",
            "code_navigation",
            "code_diagnostics",
            "parallel_task",
        ] {
            assert!(
                !registry.contains(absent),
                "{absent} must stay absent when its capability is off"
            );
        }
    }
}