How Kimi Toolset Discovers, Loads, and Executes Tools via Import Paths in kimi-cli
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, with supporting types defined in src/kimi_cli/wire/types.py, hook integration provided by src/kimi_cli/hooks/engine.py, and agent specifications parsed by 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 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, 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, 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. 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. 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
# 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},
)
# 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
# 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:ClassNameimport strings parsed byload_toolsand_load_toolinsrc/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
handlepipeline enforces validation, JSON argument parsing, and deduplication via_canonical_tool_argumentsbefore any code runs. - Hooks integrate through
HookEngineand can block execution viaPreToolUseevents. - Every tool call is executed as an async coroutine with full error wrapping, telemetry, and cleanup, producing a
ToolResultthat becomes aToolReturnValuefor 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →