Skip to content

Latest commit

 

History

History
571 lines (430 loc) · 14 KB

File metadata and controls

571 lines (430 loc) · 14 KB

@moonshot-ai/kimi-agent-sdk

TypeScript SDK for interacting with Kimi Code CLI via wire protocol.

Installation

npm install @moonshot-ai/kimi-agent-sdk zod
# or
pnpm add @moonshot-ai/kimi-agent-sdk zod

Note: This SDK requires zod (v3.24.0+ or v4.x) as a peer dependency. You need to install it in your project.

Quick Start

import { createSession } from '@moonshot-ai/kimi-agent-sdk';

const session = createSession({
  workDir: '/path/to/project',
  model: 'kimi-latest',
  thinking: true,
});

const turn = session.prompt('Explain this codebase');

for await (const event of turn) {
  if (event.type === 'ContentPart' && event.payload.type === 'text') {
    process.stdout.write(event.payload.text);
  }
}

await session.close();

API Reference

Session Management

createSession(options: SessionOptions): Session

Creates a new session instance.

interface SessionOptions {
  workDir: string;           // Working directory path
  sessionId?: string;        // Optional session ID (auto-generated if omitted)
  model?: string;            // Model identifier
  thinking?: boolean;        // Enable thinking mode
  yoloMode?: boolean;        // Auto-approve all tool calls
  executable?: string;       // Path to CLI executable (default: "kimi")
  env?: Record<string, string>; // Environment variables
}

Session

interface Session {
  readonly sessionId: string;
  readonly workDir: string;
  readonly state: SessionState;  // 'idle' | 'active' | 'closed'
  
  // Configurable properties
  model: string | undefined;
  thinking: boolean;
  yoloMode: boolean;
  executable: string;
  env: Record<string, string>;
  
  // Methods
  prompt(content: string | ContentPart[]): Turn;
  close(): Promise<void>;
  [Symbol.asyncDispose](): Promise<void>;
}

Turn

Represents an ongoing conversation turn.

interface Turn {
  [Symbol.asyncIterator](): AsyncIterator<StreamEvent, RunResult, undefined>;
  interrupt(): Promise<void>;
  approve(requestId: string, response: ApprovalResponse): Promise<void>;
  readonly result: Promise<RunResult>;
}

prompt(content, options): Promise<{ result, events }>

One-shot prompt helper for simple use cases.

import { prompt } from '@moonshot-ai/kimi-agent-sdk';

const { result, events } = await prompt('What does this code do?', {
  workDir: '/path/to/project',
  model: 'kimi-latest',
});

Stream Events

Events emitted during a turn:

Event Type Payload Description
TurnBegin { user_input } Turn started
StepBegin { n } New step started
StepInterrupted {} Step was interrupted
ContentPart ContentPart Text or thinking content
ToolCall ToolCall Tool invocation started
ToolCallPart { arguments_part } Streaming tool arguments
ToolResult ToolResult Tool execution result
SubagentEvent SubagentEvent Nested agent event
StatusUpdate StatusUpdate Token usage and context info
CompactionBegin {} Context compaction started
CompactionEnd {} Context compaction finished
ApprovalRequest ApprovalRequestPayload Tool needs approval

Content Types

ContentPart

type ContentPart =
  | { type: 'text'; text: string }
  | { type: 'think'; think: string; encrypted?: string | null }
  | { type: 'image_url'; image_url: { url: string; id?: string | null } }
  | { type: 'audio_url'; audio_url: { url: string; id?: string | null } }
  | { type: 'video_url'; video_url: { url: string; id?: string | null } };

ToolCall

interface ToolCall {
  type: 'function';
  id: string;
  function: {
    name: string;
    arguments: string | null;
  };
  extras?: Record<string, unknown> | null;
}

ToolResult

interface ToolResult {
  tool_call_id: string;
  return_value: {
    is_error: boolean;
    output: string | ContentPart[];
    message: string;
    display: DisplayBlock[];
    extras?: Record<string, unknown> | null;
  };
}

DisplayBlock

type DisplayBlock =
  | { type: 'brief'; text: string }
  | { type: 'diff'; path: string; old_text: string; new_text: string }
  | { type: 'todo'; items: Array<{ title: string; status: 'pending' | 'in_progress' | 'done' }> }
  | { type: string; data: Record<string, unknown> };  // Unknown block

RunResult

interface RunResult {
  status: 'finished' | 'cancelled' | 'max_steps_reached';
  steps?: number;
}

ApprovalResponse

type ApprovalResponse = 'approve' | 'approve_for_session' | 'reject';

Session Storage

listSessions(workDir: string): Promise<SessionInfo[]>

Lists all sessions for a workspace.

interface SessionInfo {
  id: string;
  workDir: string;
  contextFile: string;
  updatedAt: number;   // Timestamp in milliseconds
  brief: string;       // First user message preview
}

deleteSession(workDir: string, sessionId: string): Promise<boolean>

Deletes a session. Returns true if successful.

parseSessionEvents(workDir: string, sessionId: string): Promise<StreamEvent[]>

Parses and returns all events from a session's history.


Configuration

parseConfig(): KimiConfig

Reads and parses the CLI configuration file.

interface KimiConfig {
  defaultModel: string | null;
  defaultThinking: boolean;
  models: ModelConfig[];
}

interface ModelConfig {
  id: string;
  name: string;
  capabilities: string[];  // 'thinking' | 'always_thinking' | 'image_in' | 'video_in'
}

saveDefaultModel(modelId: string, thinking?: boolean): void

Updates the default model in the configuration file.

getModelById(models: ModelConfig[], modelId: string): ModelConfig | undefined

Finds a model by ID.

getModelThinkingMode(model: ModelConfig): ThinkingMode

Returns the thinking mode for a model.

type ThinkingMode = 'none' | 'switch' | 'always';

isModelThinking(models: ModelConfig[], modelId: string): boolean

Checks if a model supports thinking.


MCP Server Management

authMCP(serverName: string, executable?: string): Promise<void>

Initiates OAuth authentication for an MCP server.

resetAuthMCP(serverName: string, executable?: string): Promise<void>

Resets authentication for an MCP server.

testMCP(serverName: string, executable?: string): Promise<MCPTestResult>

Tests connection to an MCP server.

interface MCPTestResult {
  success: boolean;
  message?: string;
  tools?: string[];
  error?: string;
}

MCPServerConfig

interface MCPServerConfig {
  name: string;
  transport: 'http' | 'stdio';
  url?: string;              // For HTTP transport
  command?: string;          // For stdio transport
  args?: string[];
  env?: Record<string, string>;
  headers?: Record<string, string>;
  auth?: 'oauth';
}

File Paths

KimiPaths

Utility object for Kimi CLI file paths.

const KimiPaths = {
  home: string;                                    // ~/.kimi
  config: string;                                  // ~/.kimi/config.toml
  mcpConfig: string;                               // ~/.kimi/mcp.json
  sessionsDir(workDir: string): string;            // Session storage directory
  sessionDir(workDir: string, sessionId: string): string;
  shadowGitDir(workDir: string, sessionId: string): string;
};

Error Handling

All errors extend AgentSdkError:

abstract class AgentSdkError extends Error {
  abstract readonly code: string;
  abstract readonly category: ErrorCategory;
  readonly cause?: unknown;
  readonly context?: Record<string, unknown>;
}

type ErrorCategory = 'transport' | 'protocol' | 'session' | 'cli';

Error Classes

Class Category Codes
TransportError transport SPAWN_FAILED, STDIN_NOT_WRITABLE, PROCESS_CRASHED, CLI_NOT_FOUND, ALREADY_STARTED, HANDSHAKE_TIMEOUT
ProtocolError protocol INVALID_JSON, SCHEMA_MISMATCH, UNKNOWN_EVENT_TYPE, UNKNOWN_REQUEST_TYPE, REQUEST_TIMEOUT, REQUEST_CANCELLED
SessionError session SESSION_CLOSED, SESSION_BUSY, TURN_INTERRUPTED, APPROVAL_FAILED
CliError cli INVALID_STATE, LLM_NOT_SET, LLM_NOT_SUPPORTED, CHAT_PROVIDER_ERROR, UNKNOWN

Error Utilities

// Check if error is from this SDK
isAgentSdkError(err: unknown): err is AgentSdkError

// Get error code (returns 'UNKNOWN' for non-SDK errors)
getErrorCode(err: unknown): string

// Get error category (returns 'unknown' for non-SDK errors)
getErrorCategory(err: unknown): ErrorCategory | 'unknown'

Utility Functions

extractBrief(display?: DisplayBlock[]): string

Extracts brief text from display blocks.

extractTextFromContentParts(parts: ContentPart[]): string

Extracts all text content from content parts.

formatContentOutput(output: string | ContentPart[]): string

Formats content output as a string.


Usage Examples

Creating External Tools

import { z } from 'zod';
import { createExternalTool, createSession } from '@moonshot-ai/kimi-agent-sdk';

// Define your custom tool with zod schema
const weatherTool = createExternalTool({
  name: 'get_weather',
  description: 'Get weather information for a city',
  parameters: z.object({
    city: z.string().describe('City name'),
    unit: z.enum(['celsius', 'fahrenheit']).optional(),
  }),
  handler: async (params) => {
    // Your tool logic here
    const weather = await fetchWeather(params.city, params.unit);
    return {
      output: `Weather in ${params.city}: ${weather.temp}°`,
      message: 'Weather fetched successfully',
    };
  },
});

// Use the tool in a session
const session = createSession({
  workDir: process.cwd(),
  externalTools: [weatherTool],
});

const turn = session.prompt('What is the weather in Beijing?');
for await (const event of turn) {
  if (event.type === 'ContentPart' && event.payload.type === 'text') {
    console.log(event.payload.text);
  }
}

Note: Works with both zod v3 and v4. The SDK will use your project's zod version.

Handling Tool Approvals

const turn = session.prompt('Delete all .tmp files');

for await (const event of turn) {
  if (event.type === 'ApprovalRequest') {
    const { id, action, description } = event.payload;
    console.log(`Approval needed: ${action} - ${description}`);
    
    // Approve or reject
    await turn.approve(id, 'approve');
  }
}

Streaming with Token Usage

for await (const event of turn) {
  if (event.type === 'StatusUpdate') {
    const { token_usage, context_usage } = event.payload;
    if (token_usage) {
      console.log(`Tokens: ${token_usage.input_other} in, ${token_usage.output} out`);
    }
  }
}

Handling Subagent Events

for await (const event of turn) {
  if (event.type === 'SubagentEvent') {
    const { parent_tool_call_id, event: subEvent } = event.payload;
    console.log(`Subagent ${parent_tool_call_id}: ${subEvent.type}`);
  }
}

Interrupting a Turn

const turn = session.prompt('Long running task...');

// Interrupt after 10 seconds
setTimeout(() => turn.interrupt(), 10000);

for await (const event of turn) {
  // Handle events until interrupted
}

const result = await turn.result;
console.log(result.status);  // 'cancelled'

Multi-turn Conversation with Image Input

import { createSession, type ContentPart } from '@moonshot-ai/kimi-agent-sdk';

async function analyzeImage() {
  const session = createSession({
    workDir: process.cwd(),
    model: 'kimi-vision',
    thinking: true,
  });

  // First turn: send image with question
  const imageContent: ContentPart[] = [
    { type: 'text', text: 'What is shown in this image?' },
    { type: 'image_url', image_url: { url: 'data:image/png;base64,iVBORw0KGgo...' } },
  ];

  const turn1 = session.prompt(imageContent);
  for await (const event of turn1) {
    if (event.type === 'ContentPart' && event.payload.type === 'text') {
      process.stdout.write(event.payload.text);
    }
  }

  // Second turn: follow-up question (session maintains context)
  const turn2 = session.prompt('Can you identify any potential issues?');
  for await (const event of turn2) {
    if (event.type === 'ContentPart' && event.payload.type === 'text') {
      process.stdout.write(event.payload.text);
    }
  }

  await session.close();
}

Resuming a Previous Session

import { 
  createSession, 
  listSessions, 
  parseSessionEvents,
  type StreamEvent 
} from '@moonshot-ai/kimi-agent-sdk';

async function resumeSession(workDir: string) {
  // List existing sessions
  const sessions = await listSessions(workDir);
  
  if (sessions.length === 0) {
    console.log('No previous sessions found');
    return;
  }

  // Get the most recent session
  const latestSession = sessions[0];
  console.log(`Resuming session: ${latestSession.brief}`);

  // Load session history
  const history = await parseSessionEvents(workDir, latestSession.id);
  
  // Display previous messages
  for (const event of history) {
    if (event.type === 'TurnBegin') {
      const input = event.payload.user_input;
      const text = typeof input === 'string' 
        ? input 
        : input.filter(p => p.type === 'text').map(p => p.text).join('\n');
      console.log(`\nUser: ${text}`);
    }
    if (event.type === 'ContentPart' && event.payload.type === 'text') {
      process.stdout.write(event.payload.text);
    }
  }

  // Create session with existing ID to continue conversation
  const session = createSession({
    workDir,
    sessionId: latestSession.id,
    model: 'kimi-latest',
  });

  // Continue the conversation
  const turn = session.prompt('Please continue from where we left off');
  for await (const event of turn) {
    if (event.type === 'ContentPart' && event.payload.type === 'text') {
      process.stdout.write(event.payload.text);
    }
  }

  await session.close();
}