# How Kimi Toolset Discovers, Loads, and Executes Tools via Import Paths in kimi-cli

> Learn how Kimi Toolset discovers and loads tools via import paths. Explore dynamic import, dependency injection, and asynchronous LLM-driven execution within kimi-cli.

- Repository: [Moonshot AI/kimi-cli](https://github.com/MoonshotAI/kimi-cli)
- Tags: internals
- Published: 2026-07-21

---

**Kimi Toolset dynamically imports tool classes from strings like `module_path:ClassName`, injects runtime dependencies, and executes LLM-driven calls asynchronously through a validated, deduplicated pipeline.**

The **Kimi Toolset** in the `MoonshotAI/kimi-cli` repository is the central registry that makes built-in, MCP, and external tools visible to the LLM runtime. It resolves **import paths** during initialization and manages the full lifecycle of a tool call from validation through async execution. The core implementation lives in [`src/kimi_cli/soul/toolset.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/soul/toolset.py), with supporting types defined in [`src/kimi_cli/wire/types.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/wire/types.py), hook integration provided by [`src/kimi_cli/hooks/engine.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/hooks/engine.py), and agent specifications parsed by [`src/kimi_cli/agentspec.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/agentspec.py).

## Tool Discovery and Loading via Import Paths

Tools are identified by an **import path** string of the form `module_path:ClassName`, for example `kimi_cli.tools.shell:Shell`. These paths are collected from agent specification YAML files by [`src/kimi_cli/agentspec.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/agentspec.py) or passed directly to `KimiToolset.load_tools` according to the `MoonshotAI/kimi-cli` source code. Built-in tools reside under `src/kimi_cli/tools/` and typically inherit from `CallableTool` or `CallableTool2`.

### Dynamic Import with `load_tools` and `_load_tool`

Inside [`src/kimi_cli/soul/toolset.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/soul/toolset.py), the `load_tools` method iterates over the provided path strings and delegates to the private helper `_load_tool` for each entry. `_load_tool` splits the string on the colon, imports the module via `importlib.import_module`, and retrieves the class with `getattr`. If the class cannot be imported or is missing, a warning is logged and the tool is skipped rather than raising a fatal error.

### Dependency Injection During Tool Instantiation

Once the class is resolved, `KimiToolset` instantiates it with automatic **dependency injection**. In [`src/kimi_cli/soul/toolset.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/soul/toolset.py), the constructor inspects `inspect.signature(tool_cls)` and fills positional parameters using objects from the `dependencies` mapping, such as the logger, config, or runtime handles. Keyword-only parameters halt this injection, ensuring that user-supplied arguments are reserved for the actual `call` invocation. Successfully created instances are registered in the internal `_tool_dict` dictionary via `self.add(tool)`, using the tool's `name` attribute as the lookup key.

## Tool Call Execution Pipeline

When the LLM emits a tool call, the runtime dispatches it to `KimiToolset.handle`, which returns an `asyncio.Task[ToolResult]`. The method executes a strict sequence of **validation**, **deduplication**, hook checks, and async invocation.

### Context Setup and Name Validation

The handler first stores the incoming `ToolCall` in a **ContextVar** so that any nested code can retrieve it through `get_current_tool_call_or_none`. The tool name is then resolved against `_tool_dict`; if the name is absent, the pipeline returns a `ToolNotFoundError` immediately.

### Argument Parsing and Call Deduplication

The JSON payload from the LLM is parsed with `json.loads`, and failures are surfaced as a `ToolParseError`. A canonical representation of the arguments is built via `_canonical_tool_arguments`, and the composite key `(tool_name, canonical_args)` is used to detect same-step or cross-step duplicates. Duplicate calls are flagged so that the result can later be augmented with a reminder instead of re-executing the work.

### Pre-Tool Hooks and Blocking Logic

Before the tool runs, the `HookEngine` fires a `PreToolUse` event as implemented in [`src/kimi_cli/hooks/engine.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/hooks/engine.py). If any registered hook returns an `action` value of `"block"`, execution stops and a `ToolError` is returned to the caller without invoking the tool.

### Async Tool Execution and Error Wrapping

If not blocked, the tool's `call` coroutine is awaited with `await tool.call(arguments)` inside [`src/kimi_cli/soul/toolset.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/soul/toolset.py). Exceptions are caught and wrapped in a `ToolRuntimeError`, while successful outputs are wrapped in a `ToolResult`. This **async execution** boundary ensures that long-running operations do not block the main runtime loop.

### Telemetry, Post-Tool Hooks, and Cleanup

Execution time, outcome status, and any error details are sent to the **telemetry** system. A fire-and-forget `PostToolUse` hook then runs to allow side effects such as logging or metrics. If the call was flagged as a duplicate, `_append_reminder_to_return_value` injects a reminder message into the return value. Finally, the `ContextVar` is reset to its previous state and the resulting `asyncio.Task[ToolResult]` is returned to the caller, eventually streaming back to the LLM as a `ToolReturnValue`.

## Practical Code Examples

```python

# Load a built-in tool by import path

from kimi_cli.soul.toolset import KimiToolset

toolset = KimiToolset()
toolset.load_tools(
    tool_paths=["kimi_cli.tools.shell:Shell"],
    dependencies={"logger": logger, "config": config},
)

```

```python

# Handle an incoming tool call from the LLM

from kimi_cli.wire.types import ToolCall, ToolCallFunction

incoming = ToolCall(
    id="call-123",
    function=ToolCallFunction(name="shell", arguments='{"cmd":"ls -l"}')
)

result_task = toolset.handle(incoming)   # asyncio.Task[ToolResult]

result = await result_task
print(result.return_value)               # ToolOk with command output

```

```python

# Register an external tool at runtime

ok, err = toolset.register_external_tool(
    name="my_external",
    description="Calls a remote service",
    parameters={"type": "object", "properties": {"url": {"type": "string"}}},
)
assert ok, err

```

## Summary

- **Kimi Toolset** resolves tools through `module_path:ClassName` import strings parsed by `load_tools` and `_load_tool` in [`src/kimi_cli/soul/toolset.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/soul/toolset.py).
- Runtime **dependency injection** automatically supplies constructor dependencies from a mapping, while keyword-only parameters remain free for call-time arguments.
- The `handle` pipeline enforces **validation**, **JSON argument parsing**, and **deduplication** via `_canonical_tool_arguments` before any code runs.
- **Hooks** integrate through `HookEngine` and can block execution via `PreToolUse` events.
- Every tool call is executed as an **async coroutine** with full error wrapping, telemetry, and cleanup, producing a `ToolResult` that becomes a `ToolReturnValue` for the LLM.

## Frequently Asked Questions

### How does Kimi Toolset map a string like `kimi_cli.tools.shell:Shell` to a live object?

`KimiToolset` splits the string into module and class components, uses `importlib.import_module` to load the module, and retrieves the class via `getattr`. The class is then instantiated and registered in `_tool_dict` under its `name` attribute.

### What happens if a tool class constructor needs runtime objects like a logger?

Positional constructor parameters are automatically filled from the `dependencies` dictionary passed to `load_tools`. Keyword-only parameters are excluded from this injection, ensuring they remain available for user arguments during `call`.

### Can duplicate tool calls from the LLM be suppressed?

Yes. The handler builds a canonical representation of arguments and checks a `(tool_name, canonical_args)` key. Duplicate calls are identified and their return values are augmented with a reminder rather than re-executing the tool.

### Is tool execution synchronous or asynchronous?

Tool execution is fully asynchronous. `KimiToolset.handle` awaits `tool.call(arguments)` and returns an `asyncio.Task[ToolResult]`, allowing the runtime to manage concurrent tool calls without blocking.