How Kimi Toolset Loads and Executes Tools Imported by Their Path

Kimi Toolset dynamically imports tool classes using importlib.import_module, instantiates them with automatic dependency injection, and executes calls asynchronously through a centralized handle method that manages context, deduplicates requests, and integrates lifecycle hooks.

The KimiToolset class in the MoonshotAI/kimi-cli repository serves as the central registry that makes built-in, MCP, and external tools available to the LLM runtime. It bridges static tool specifications with live Python objects by resolving import paths, managing runtime dependencies, and orchestrating execution with comprehensive error handling, deduplication, and telemetry tracking.

Tool Discovery and Dynamic Loading via Import Paths

Tools in Kimi CLI are identified using an import path string following the format module_path:ClassName (e.g., kimi_cli.tools.shell:Shell). These paths originate from agent specification YAML files or direct invocations of the loading API.

The load_tools Entry Point

In src/kimi_cli/soul/toolset.py, the load_tools method (lines 83-108) iterates over provided path strings and delegates to the private _load_tool helper for each entry. This method accepts a dependencies dictionary containing runtime objects like loggers and configuration that may be injected into tool instances.

Dynamic Import Resolution

The _load_tool implementation splits the import path string on the colon delimiter, then uses importlib.import_module to load the specified module. It retrieves the class object via getattr. If the class cannot be imported or is missing from the module, the toolset logs a warning and skips the entry rather than failing the entire loading process.

Dependency Injection During Instantiation

Once the class is retrieved, the toolset inspects its __init__ signature using inspect.signature(tool_cls) (lines 31-41). Positional parameters are automatically populated with objects from the dependencies mapping passed to load_tools. Keyword-only parameters halt this injection process, allowing the tool to receive user-supplied arguments at call time. Successfully instantiated tools are registered in the internal _tool_dict dictionary via self.add(tool), using the tool's name property as the lookup key.

Executing Tool Calls in the Kimi Runtime

When the LLM emits a tool call, the runtime dispatches to KimiToolset.handle, which implements a comprehensive execution pipeline.

Context Management and Validation

The handle method first stores the current ToolCall in a ContextVar so nested code can access it via get_current_tool_call_or_none (lines 44-49). It then validates the tool name against _tool_dict, raising a ToolNotFoundError if the tool is not registered (lines 48-53).

Argument Parsing and Deduplication

The JSON arguments from the LLM are parsed using json.loads, with parsing errors wrapped in a ToolParseError (lines 54-63). Before execution, the system builds a canonical representation of the arguments via _canonical_tool_arguments. A call key constructed from (tool_name, canonical_args) enables detection of same-step or cross-step duplicates, preventing redundant executions and optionally triggering reminder text insertion (lines 65-84).

Hook Integration and Async Execution

The HookEngine fires a PreToolUse event before execution (lines 58-73). If any hook returns action="block", execution stops immediately with a ToolError. Otherwise, the tool's call coroutine is awaited: await tool.call(arguments) (lines 84-110). Exceptions during execution are caught and wrapped in a ToolRuntimeError, while successful results are encapsulated in a ToolResult object.

Telemetry and Cleanup

Execution metrics, outcomes, and errors are sent to the telemetry system, followed by a fire-and-forget PostToolUse hook (lines 110-130). If the call was identified as a duplicate, the return value is augmented with a reminder message via _append_reminder_to_return_value (lines 94-108). Finally, the ContextVar is reset to its previous state, and the resulting asyncio.Task[ToolResult] is returned to the caller (lines 129-134).

Practical Implementation Examples

The following examples demonstrate how to load tools and handle incoming calls from the LLM runtime:


# Loading built-in tools with dependency injection

from kimi_cli.soul.toolset import KimiToolset

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

# Handling 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"}')
)

# handle() returns an asyncio.Task[ToolResult]

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

# Registering an external (wire) 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 tool specifications using module_path:ClassName strings processed by importlib.import_module in src/kimi_cli/soul/toolset.py.
  • The load_tools method orchestrates discovery and instantiates tools with automatic dependency injection based on inspect.signature analysis.
  • Tool execution occurs through the async handle method, which sets up request context via ContextVar, validates tool existence against _tool_dict, and parses JSON arguments.
  • Built-in deduplication logic using _canonical_tool_arguments prevents redundant executions by tracking unique (tool_name, arguments) pairs across steps.
  • The execution pipeline integrates PreToolUse and PostToolUse hooks via the HookEngine, with comprehensive telemetry collection and error wrapping in ToolRuntimeError.

Frequently Asked Questions

What file format does Kimi Toolset use to specify tool import paths?

Kimi Toolset uses a colon-separated string format of module_path:ClassName (e.g., kimi_cli.tools.shell:Shell). These strings are typically defined in agent specification YAML files under the tools: section or passed directly to the load_tools method. The system splits this string to dynamically import the module using importlib.import_module and retrieve the class via getattr.

How does Kimi Toolset inject dependencies into tool instances during loading?

During instantiation in _load_tool, the toolset inspects the tool class's __init__ signature using inspect.signature (lines 31-41). It automatically maps positional parameters to objects provided in the dependencies dictionary (such as loggers or configuration). Keyword-only parameters in the signature stop this injection, ensuring they receive runtime arguments from the actual tool call rather than dependencies.

What happens when a tool execution fails or the requested tool is not found?

If the tool name is not found in _tool_dict, KimiToolset.handle returns a ToolNotFoundError immediately (lines 48-53). If JSON argument parsing fails, it returns a ToolParseError. During execution, any unhandled exceptions from await tool.call() are caught and wrapped in a ToolRuntimeError. These error types are part of the ToolResult structure returned to the LLM runtime, allowing the system to report specific failure modes to the agent.

How does the toolset prevent duplicate tool executions within the same conversation?

The handle method computes a canonical representation of the tool arguments using _canonical_tool_arguments (lines 65-84) and constructs a unique call key combining the tool name and these canonical args. If the same key appears within a step or across recent steps, the system identifies it as a duplicate, prevents redundant execution, and optionally appends a reminder message to the return value via _append_reminder_to_return_value to inform the LLM of the repetition.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →