npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yushaw/sanqian-sdk

v0.3.28

Published

Sanqian SDK for third-party app integration

Downloads

783

Readme

Sanqian Developer Guide / 三千开发者指南

English | 中文


English

Sanqian provides two ways to integrate with external applications:

| Method | Use Case | Protocol | |--------|----------|----------| | HTTP API | Simple chat, any language | REST + SSE | | SDK | Tool registration, agents, context injection, deep integration | WebSocket |

Quick Start

HTTP API

# Get port (Sanqian writes it on startup)
PORT=$(cat ~/.sanqian/runtime/api.port)

# Chat with default agent
curl -X POST "http://localhost:$PORT/api/agents/default/chat" \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "Hello"}], "stream": false}'

SDK

npm install @yushaw/sanqian-sdk
import { SanqianSDK } from '@yushaw/sanqian-sdk'

const sdk = new SanqianSDK({
  appName: 'my-app',
  appVersion: '1.0.0',
  tools: [{
    name: 'greet',
    description: 'Greet a user',
    parameters: {
      type: 'object',
      properties: { name: { type: 'string' } },
      required: ['name']
    },
    handler: async ({ name }) => `Hello, ${name}!`
  }]
})

// SDK auto-connects when Sanqian starts (via connection.json file watching).
// If Sanqian is already running, it connects immediately.
// If autoLaunchSanqian is true (default), it launches Sanqian if not running.

Tools

Tools are the primary way your app provides capabilities to Sanqian. When a user asks a question, the AI agent can call your tools to get data or perform actions.

Define Tools

Each tool needs a name, description (for the LLM), JSON Schema parameters, and a handler function.

const sdk = new SanqianSDK({
  appName: 'my-notes-app',
  appVersion: '1.0.0',
  tools: [
    {
      name: 'search_notes',
      description: 'Search through user notes by keyword',
      parameters: {
        type: 'object',
        properties: {
          query: { type: 'string', description: 'Search keyword' },
          limit: { type: 'number', description: 'Max results', default: 10 }
        },
        required: ['query']
      },
      handler: async ({ query, limit }) => {
        const results = await db.searchNotes(query, limit)
        return results.map(n => ({ title: n.title, snippet: n.snippet }))
      }
    },
    {
      name: 'create_note',
      description: 'Create a new note',
      parameters: {
        type: 'object',
        properties: {
          title: { type: 'string' },
          content: { type: 'string' }
        },
        required: ['title', 'content']
      },
      handler: async ({ title, content }) => {
        const note = await db.createNote(title, content)
        return { id: note.id, title: note.title }
      }
    }
  ]
})

Tool names are automatically prefixed with your app name. search_notes becomes my-notes-app:search_notes in Sanqian.

Update Tools at Runtime

You can add, remove, or modify tools after initialization:

await sdk.updateTools([
  { name: 'search_notes', /* updated definition */ handler: newHandler },
  { name: 'new_tool', /* ... */ handler: newToolHandler }
])

This replaces the entire tool list. The change takes effect immediately for new agent runs.

Tool Searchability

By default, tools are discoverable via Sanqian's search_capability tool (used by agents to find relevant tools). Set searchable: false to hide a tool from search while keeping it available:

{
  name: 'internal_sync',
  description: 'Internal data sync',
  searchable: false,  // Available but not discoverable via search
  parameters: { type: 'object' },
  handler: async () => { /* ... */ }
}

Agents

Private agents are custom AI personas with specific tools, skills, and instructions. They appear in Sanqian's agent picker.

Create an Agent

const agent = await sdk.createAgent({
  agent_id: 'notes-assistant',
  name: 'Notes Assistant',
  description: 'Helps manage and search notes',
  system_prompt: 'You are a notes assistant. Help users organize their notes.',
  tools: ['search_notes', 'create_note'],  // Your SDK tools (auto-prefixed)
  skills: ['web-research'],                 // Sanqian built-in skills
  subagents: ['*'],                         // Can delegate to any agent
  searchable: true,                         // Discoverable by other agents
})

console.log(agent.agent_id) // "my-notes-app:notes-assistant"

Tool Name Formats

The tools array in agent config supports multiple formats:

| Format | Example | Description | |--------|---------|-------------| | Short name | "search_notes" | Your SDK tool (auto-prefixed with app name) | | Full SDK name | "other-app:tool_name" | Another SDK app's tool | | Built-in | "read_file", "run_bash_command" | Sanqian built-in tools | | MCP | "mcp_servername_toolname" | MCP server tools | | Wildcard | ["*"] | All available tools |

Sub-agents

Control whether your agent can delegate tasks to other agents:

{
  subagents: undefined,          // Cannot use task tool (default)
  subagents: [],                 // Cannot use task tool (explicit)
  subagents: ['*'],              // Can call any agent
  subagents: ['agent1', 'agent2'] // Can only call specific agents
}

Update and Delete

// Update specific fields (others unchanged)
await sdk.updateAgent('notes-assistant', {
  system_prompt: 'Updated instructions...',
  tools: ['search_notes', 'create_note', 'delete_note']
})

// Delete
await sdk.deleteAgent('notes-assistant')

// List your agents
const agents = await sdk.listAgents()

Chat

Send messages to any agent and get responses. Supports both streaming and non-streaming modes.

Non-streaming

const response = await sdk.chat('notes-assistant', [
  { role: 'user', content: 'Find my notes about TypeScript' }
])

console.log(response.message.content)
console.log(response.conversationId)  // Empty string if stateless

Streaming

for await (const event of sdk.chatStream('notes-assistant', [
  { role: 'user', content: 'Summarize my recent notes' }
])) {
  switch (event.type) {
    case 'start':
      // Stream started, event.run_id available
      break
    case 'text':
      process.stdout.write(event.content || '')
      break
    case 'thinking':
      // Reasoning content (DeepSeek R1, o3, etc.)
      break
    case 'tool_call':
      console.log(`Calling: ${event.tool_call?.function.name}`)
      break
    case 'tool_args_chunk':
      // Streaming tool arguments (partial JSON)
      break
    case 'tool_result':
      console.log(`Result: ${event.success ? 'ok' : event.error}`)
      break
    case 'done':
      console.log(`\nConversation: ${event.conversationId}`)
      break
    case 'error':
      console.error(event.error)
      break
  }
}

Stream Events

| Event | Fields | Description | |-------|--------|-------------| | start | run_id, conversationId | Stream started | | text | content | Text chunk from the AI | | thinking | content | Reasoning content (thinking models) | | tool_call | tool_call | Tool invocation started | | tool_args_chunk | tool_call_id, tool_name, chunk | Streaming tool arguments | | tool_args | tool_call_id, tool_name, args | Complete tool arguments | | tool_result | tool_call_id, result, success, error | Tool execution result | | interrupt | interrupt_type, interrupt_payload, run_id | HITL pause (see below) | | done | conversationId, title | Stream finished | | error | error | Error occurred | | cancelled | run_id | Run was cancelled |

Stateful vs Stateless

// Stateless: you manage message history
const r1 = await sdk.chat('agent', messages)
// r1.conversationId is empty string

// Stateful: server manages history
const r1 = await sdk.chat('agent', messages, {
  persistHistory: true  // Creates a server-side conversation
})
// r1.conversationId is set, use it for follow-ups

const r2 = await sdk.chat('agent', [{ role: 'user', content: 'Follow up' }], {
  conversationId: r1.conversationId
})

Conversation Helper

For multi-turn conversations, use the Conversation helper:

const conv = sdk.startConversation('notes-assistant')

const r1 = await conv.send('Find my TypeScript notes')
console.log(r1.message.content)

const r2 = await conv.send('Summarize the first one')
console.log(conv.id)  // Conversation ID (available after first send)

// Streaming
for await (const event of conv.sendStream('Any more details?')) {
  if (event.type === 'text') process.stdout.write(event.content || '')
}

// Get history
const details = await conv.getDetails({ messageLimit: 50 })

// Delete
await conv.delete()

Cancel a Run

let runId: string | undefined

for await (const event of sdk.chatStream('agent', messages)) {
  if (event.type === 'start') {
    runId = event.run_id
  }
  if (shouldCancel) {
    sdk.cancelRun(runId!)
    break
  }
}

Auto-discovery

Enable automatic discovery of skills, tools, or sub-agents for a chat call:

const response = await sdk.chat('agent', messages, {
  autoDiscoverSkills: true,   // Agent can find and use skills
  autoDiscoverTools: true,    // Agent can find and use tools
  autoDiscoverSubagents: true // Agent can delegate to other agents
})

Local file ingest

For same-machine deployments where the Sanqian backend can read a local path directly, use uploadFile() first, then pass the returned file.path into chat():

const uploaded = await sdk.uploadFile('/absolute/path/report.pdf', {
  conversationId,
  autoIndex: true,
  asyncProcess: true,
})

const response = await sdk.chat('agent', messages, {
  conversationId: uploaded.conversationId,
  filePaths: [uploaded.file.path],
})

uploadFile(localPath) is not a remote file transfer protocol. For remote/backend-on-another-machine cases, continue using explicit byte upload APIs.


Human-in-the-Loop (HITL)

HITL allows the agent to pause and ask for user input during a run. There are three interrupt types:

| Type | Use Case | Example | |------|----------|---------| | approval_request | Pre-execution approval | "Delete file X?" -> Approve/Reject | | user_input_request | Ask for user input | "What format?" -> ["JSON", "CSV"] or free text | | user_action_request | User completes external action | "Login required" -> User logs in -> "Done" |

Handling Interrupts

for await (const event of sdk.chatStream('agent', messages)) {
  if (event.type === 'interrupt') {
    const { interrupt_type, interrupt_payload, run_id } = event

    if (interrupt_type === 'approval_request') {
      const approved = await showApprovalDialog(interrupt_payload)
      sdk.sendHitlResponse(run_id!, { approved })
    }

    if (interrupt_type === 'user_input_request') {
      const answer = await showInputDialog(interrupt_payload)
      sdk.sendHitlResponse(run_id!, { answer })
    }

    if (interrupt_type === 'user_action_request') {
      await waitForUserAction(interrupt_payload)
      sdk.sendHitlResponse(run_id!, { approved: true })
    }
  }
}

HITL Response Options

sdk.sendHitlResponse(runId, {
  approved: true,           // For approval_request
  remember: true,           // Remember this choice for future
  answer: 'JSON',           // For user_input_request (text)
  selected_indices: [0, 2], // For multi-select options
  cancelled: true,          // User cancelled
  timed_out: true,          // Request timed out
})

Conversations

Manage conversation history stored on the server.

// List conversations (optionally filter by agent)
const { conversations, total } = await sdk.listConversations({
  agentId: 'notes-assistant',
  limit: 20,
  offset: 0
})

// Get conversation with messages
const detail = await sdk.getConversation(conversationId, {
  includeMessages: true,
  messageLimit: 50,
  messageOffset: 0
})

// Get message history (via HTTP API, aligned with main app)
const history = await sdk.getMessages(conversationId, {
  limit: 50,
  offset: 0
})
// Returns: { messages, has_more, session_id, returned_turns }

// Delete a conversation
await sdk.deleteConversation(conversationId)

Context Providers

Context providers let your app inject dynamic state into conversations. When the user (or agent) attaches a context, Sanqian calls your provider to fetch the latest data.

Three provider methods, all optional:

| Method | Purpose | When Called | |--------|---------|------------| | getCurrent() | Get current state | User sends message with context attached | | getList(options) | List available resources | User opens "+" menu to browse | | getById(id) | Get specific resource | User selects item from list |

Register Context Providers

const sdk = new SanqianSDK({
  appName: 'my-notes-app',
  appVersion: '1.0.0',
  tools: [/* ... */],
  contexts: [
    {
      id: 'active-note',
      name: 'Active Note',
      description: 'The note currently being edited',
      getCurrent: async () => ({
        content: editor.getCurrentNote().content,
        title: editor.getCurrentNote().title,
        type: 'note',
      }),
    },
    {
      id: 'notes',
      name: 'Notes Library',
      description: 'Browse and attach notes',
      getList: async (options) => {
        const notes = await db.searchNotes(options?.query || '', {
          offset: options?.offset || 0,
          limit: options?.limit || 20,
        })
        return {
          items: notes.map(n => ({
            id: n.id,
            title: n.title,
            summary: n.snippet,
            type: 'note',
            group: n.folder,
          })),
          hasMore: notes.length === (options?.limit || 20),
        }
      },
      getById: async (id) => {
        const note = await db.getNote(id)
        if (!note) return null
        return {
          id: note.id,
          content: note.content,
          title: note.title,
          type: 'note',
        }
      },
    }
  ]
})

Attach Contexts to Agents

Auto-attach context providers to an agent so they're always included:

await sdk.createAgent({
  agent_id: 'notes-assistant',
  name: 'Notes Assistant',
  tools: ['search_notes'],
  attached_contexts: ['active-note'],  // Auto-prefixed with app name
})

Update Contexts at Runtime

await sdk.updateContexts([
  { id: 'active-note', name: 'Active Note', description: '...', getCurrent: newHandler },
  { id: 'new-context', name: 'New Context', description: '...', getList: listHandler },
])

Context Data Format

The ContextData object returned by providers:

| Field | Required | Description | |-------|----------|-------------| | content | Yes | Content injected into conversation | | id | No | Resource ID (for getById scenario) | | title | No | Display title | | summary | No | UI preview text | | version | No | Change detection (defaults to content hash) | | type | No | Resource type for display styling | | metadata | No | Extra data accessible in templates | | template | No | Custom Mustache template: "# {{title}}\n\n{{content}}" |


Session Resources

Session resources are temporary context your app pushes to Sanqian. Unlike context providers (pull-based), session resources are push-based: your app decides when to send data.

They're visible in all Chat UI instances and persist until your app disconnects or removes them.

// Push a resource
const stored = await sdk.pushResource({
  title: 'Current Note',
  content: '<note>\n# My Note\nContent here...\n</note>',
  summary: 'My Note - 2024-01-15',
  icon: '📝',
  type: 'note',
})
console.log(stored.fullId) // "my-notes-app:abc123"

// Remove a specific resource
await sdk.removeResource('my-notes-app:abc123')

// Clear all your app's resources
await sdk.clearResources()

// Get local cache (no server round-trip)
const resources = sdk.getSessionResources()

// Fetch from server (optionally filtered by agent)
const serverResources = await sdk.fetchSessionResources('notes-assistant')

Attach Session Resources to Chat

for await (const event of sdk.chatStream('agent', messages, {
  sessionResources: ['my-notes-app:abc123'],  // Include specific resources
})) {
  // ...
}

Listen for Removal

When a user removes a session resource from the Chat UI:

sdk.on('resourceRemoved', (resourceId) => {
  console.log(`User removed: ${resourceId}`)
})

Capability Discovery

Query Sanqian's full capability registry: tools, skills, agents, and context providers.

// List all tools
const tools = await sdk.listTools()

// List by source
const builtinTools = await sdk.listTools('builtin')
const sdkTools = await sdk.listTools('sdk')
const mcpTools = await sdk.listTools('mcp')

// List skills
const skills = await sdk.listSkills()

// List all available agents (not just your own)
const agents = await sdk.listAvailableAgents()

// Generic listing with filters
const caps = await sdk.listCapabilities({
  type: 'tool',        // 'tool' | 'skill' | 'agent' | 'context' | 'all'
  source: 'builtin',   // Filter by source
  category: 'file',    // Filter by category (tools only)
})

// Semantic search (BM25 + Vector hybrid)
const results = await sdk.searchCapabilities('file operations', {
  type: 'tool',
  limit: 5,
})
for (const r of results) {
  console.log(`${r.capability.id}: score=${r.score}`)
  console.log(`  How to use: ${r.howToUse}`)
}

Embedding & Rerank Config

Reuse the embedding and rerank models configured in Sanqian. Useful for apps that need vector search or document ranking without managing their own API keys.

// Embedding
const embedding = await sdk.getEmbeddingConfig()
if (embedding.available) {
  // Use embedding.apiUrl, embedding.apiKey, embedding.modelName, embedding.dimensions
}

// Rerank
const rerank = await sdk.getRerankConfig()
if (rerank.available) {
  // Use rerank.apiUrl, rerank.apiKey, rerank.modelName
}

Messaging Channels API

Channels methods in the SDK use Sanqian HTTP endpoints with X-App-Token auth. They only require runtime connection info and do not require WebSocket registration to be completed first.

// Useful when building URLs for local HTTP integrations
const baseUrl = sdk.getBaseUrl() // e.g. http://127.0.0.1:8765

// Fetch config schema for setup forms
const schema = await sdk.getChannelConfigSchema('telegram')

// Validate/probe config before creating an account
const probe = await sdk.probeChannelConfig({
  channel_type: 'telegram',
  config: { bot_token: process.env.TELEGRAM_BOT_TOKEN }
})
if (!probe.ok) {
  console.error(probe.error)
}

This section only shows the newly added methods. Full account/binding/message operations are available via listChannelAccounts, createChannelAccount, listChannelBindings, sendChannelMessage, and related methods.


Connection

Connection Lifecycle

The SDK manages connection automatically:

  1. Constructor: Reads ~/.sanqian/runtime/connection.json and watches for changes
  2. Auto-connect: When connection.json appears (Sanqian starts), SDK connects automatically
  3. Auto-launch: If autoLaunchSanqian: true (default), SDK starts Sanqian if not running
  4. Registration: After WebSocket connects, SDK registers app name, tools, and context providers
  5. Heartbeat: 30-second interval to detect dead connections
  6. Auto-reconnect: Exponential backoff (500ms to 5s) with jitter when connection drops
connection.json appears -> WebSocket connect -> Register -> Heartbeat
        ^                                                      |
        |                  Reconnect (backoff)                 v
        +<------------------  Disconnect  <-------- Heartbeat timeout

Reconnect Control

Auto-reconnect is reference-counted. Enable it when your UI needs a persistent connection:

// Chat panel opens - request persistent connection
sdk.acquireReconnect()

// Chat panel closes - release
sdk.releaseReconnect()

// When refCount reaches 0, auto-reconnect stops

Connection State

const state = sdk.getState()
// { connected: boolean, registering: boolean, registered: boolean,
//   lastError?: Error, reconnectAttempts: number }

sdk.isConnected() // true when connected AND registered

Events

sdk.on('connected', () => { /* WebSocket opened */ })
sdk.on('disconnected', (reason) => { /* Connection lost */ })
sdk.on('registered', () => { /* Tools and contexts registered */ })
sdk.on('error', (error) => { /* Connection error */ })
sdk.on('tool_call', ({ name, arguments }) => { /* Tool invoked (for logging) */ })
sdk.on('resourcePushed', (resource) => { /* Session resource pushed */ })
sdk.on('resourceRemoved', (resourceId) => { /* Session resource removed by user */ })
sdk.on('resourcesCleared', (appName) => { /* All resources cleared */ })

// One-time listener
sdk.once('registered', () => { /* ... */ })

// Remove listeners
sdk.off('connected', handler)
sdk.removeAllListeners()        // All events
sdk.removeAllListeners('error') // Specific event

Manual Connection

// Connect (usually not needed - SDK auto-connects)
await sdk.connect()

// Disconnect
await sdk.disconnect()

Launched by Sanqian

When Sanqian launches your app (via launchCommand), it sets SANQIAN_LAUNCHED=1. The SDK detects this and connects immediately without auto-reconnect (Sanqian manages the lifecycle).

SDKConfig Options

const sdk = new SanqianSDK({
  // Required
  appName: 'my-app',
  appVersion: '1.0.0',
  tools: [],

  // Display
  displayName: 'My App',           // Shown in Sanqian UI

  // Launch
  launchCommand: '/path/to/app',   // For Sanqian to start your app
  metadata: { browser: 'chrome' }, // App metadata stored with registration

  // Context
  contexts: [],                    // Context providers (see above)

  // Timeouts
  reconnectInterval: 5000,         // Reconnect interval (ms, default: 5000)
  heartbeatInterval: 30000,        // Heartbeat interval (ms, default: 30000)
  toolExecutionTimeout: 30000,     // Tool timeout (ms, default: 30000)
  connectionFileReconnectMaxDurationMs: 300000, // File-watch reconnect max duration (ms, default: 5min)
  connectionFileReconnectJitterMs: 300,         // Extra random delay per retry (ms, default: 300)

  // Auto-launch
  autoLaunchSanqian: true,         // Launch Sanqian if not running (default: true)
  sanqianPath: '/path/to/Sanqian', // Custom executable path (optional)

  // Debug
  debug: false,                    // Console logging (default: false)

  // Browser mode (see Browser Build section)
  connectionInfo: undefined,       // Pre-configured connection (skips file discovery)
})

Browser Build

For browser environments (extensions, web apps, Office Add-ins), use the browser-specific import:

import { SanqianSDK } from '@yushaw/sanqian-sdk/browser'

The browser build:

  • Uses native WebSocket (no Node.js ws dependency)
  • Requires connectionInfo in config (no filesystem access for discovery)
  • Does not support autoLaunchSanqian or connection.json file watching
  • Supports all other features: tools, agents, chat, context providers, session resources, HITL, capability discovery
import { SanqianSDK } from '@yushaw/sanqian-sdk/browser'

const sdk = new SanqianSDK({
  appName: 'my-extension',
  appVersion: '1.0.0',
  connectionInfo: {
    port: 38765,
    token: 'your-token',
    ws_path: '/ws/apps',
    version: 1,
    pid: 0,
    started_at: '',
  },
  tools: [/* ... */]
})

await sdk.connect()

HTTP API Reference

For simple integrations without the SDK. Base URL: http://localhost:{PORT} (port from ~/.sanqian/runtime/api.port).

Chat

POST /api/agents/{agent_id}/chat

Request body:

| Field | Type | Required | Description | |-------|------|----------|-------------| | messages | ChatMessage[] | Yes | Messages to send | | stream | boolean | No | Enable SSE streaming (default: true) | | conversation_id | string | No | Continue existing conversation |

Non-streaming response (stream: false):

{
  "message": { "role": "assistant", "content": "..." },
  "conversation_id": "abc123"
}

Streaming response (stream: true, SSE):

data: {"type": "text", "content": "Hello"}
data: {"type": "text", "content": " world"}
data: {"type": "tool_call", "tool_call": {...}}
data: {"type": "tool_result", "tool_result": {...}}
data: {"type": "done", "conversation_id": "abc123"}

List Agents

GET /api/agents

Returns all available agents:

[
  { "id": "default", "name": "Default", "description": "..." },
  { "id": "coding", "name": "Coding", "description": "..." }
]

Get Agent

GET /api/agents/{agent_id}

Examples

Python:

import requests, json

port = open('~/.sanqian/runtime/api.port').read().strip()

# Non-streaming
r = requests.post(f'http://localhost:{port}/api/agents/default/chat', json={
    'messages': [{'role': 'user', 'content': 'Hello'}],
    'stream': False,
})
print(r.json()['message']['content'])

# Streaming
r = requests.post(f'http://localhost:{port}/api/agents/default/chat', json={
    'messages': [{'role': 'user', 'content': 'Hello'}],
    'stream': True,
}, stream=True)
for line in r.iter_lines():
    if line.startswith(b'data: '):
        event = json.loads(line[6:])
        if event['type'] == 'text':
            print(event['content'], end='', flush=True)

JavaScript (no SDK):

const port = require('fs').readFileSync(
  require('os').homedir() + '/.sanqian/runtime/api.port', 'utf8'
).trim()

const response = await fetch(`http://localhost:${port}/api/agents/default/chat`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    messages: [{ role: 'user', content: 'Hello' }],
    stream: false,
  }),
})
const data = await response.json()
console.log(data.message.content)

Built-in Tools Reference

These tools are available to all agents. Use tool names in agent config to enable specific ones, or ["*"] for all.

File Operations

| Tool | Description | |------|-------------| | read_file | Read files from workspace | | write_file | Write files to workspace | | edit_file | Edit files with precise string replacement | | delete_file | Delete files from workspace | | list_files | List files in workspace directory | | find_files | Find files by name pattern (glob) | | search_file | Search for content within files | | grep_content | Cross-file content search with regex |

Web & Search

| Tool | Description | |------|-------------| | web_search | Search the web (Google Custom Search) | | fetch_web | Fetch web pages and convert to markdown |

Execution

| Tool | Description | |------|-------------| | run_bash_command | Execute shell commands in workspace (sandboxed) |

Memory

| Tool | Description | |------|-------------| | search_memory | Search user memories by semantic similarity | | save_memory | Save a new memory for the user | | list_memories | List all user memories with optional filtering |

Task & Agent

| Tool | Description | |------|-------------| | todo_write | Create and update task list | | task | Delegate a task to another agent | | search_capability | Search available tools, skills, and agents | | ask_human | Ask user for input when additional information is needed |

Vision & Image

| Tool | Description | |------|-------------| | vision_analyze | Analyze images using vision model | | generate_image | Generate images from text |

macOS Apple Integration (macOS only)

| Tool | Description | |------|-------------| | calendar_search / calendar_create / calendar_delete | Calendar operations | | notes_search / notes_create / notes_delete | Notes operations | | reminders_search / reminders_create / reminders_complete / reminders_delete | Reminders operations | | contacts_search | Search contacts |


FAQ

How do I get the agent_id for chat? Use the List Agents API (GET /api/agents) or sdk.listAgents(). Common built-in IDs: default, coding.

When to use HTTP API vs SDK? HTTP API for simple chat from any language. SDK when you need tools, agents, context providers, or session resources.

Are SDK conversations visible in Sanqian? Yes. Conversations created via SDK appear in Sanqian's conversation list.

What about tool naming conflicts? SDK tools are namespaced: appName:toolName. No conflicts between apps.

Does the agent auto-reconnect after Sanqian restarts? Yes. The Node.js SDK watches connection.json. When Sanqian restarts and writes a new file, SDK reconnects automatically.

What happens if Sanqian is not running when my app starts? With autoLaunchSanqian: true (default), SDK launches Sanqian in tray mode. Otherwise, SDK watches for connection.json and connects when Sanqian starts later.

Error Codes

| Code | Description | |------|-------------| | 400 | Invalid request parameters | | 404 | Agent or conversation not found | | 429 | Rate limited | | 500 | Internal server error |


中文

Sanqian 提供两种外部集成方式:

| 方式 | 适用场景 | 协议 | |------|----------|------| | HTTP API | 简单对话,任意语言 | REST + SSE | | SDK | 工具注册、Agent、上下文注入、深度集成 | WebSocket |

快速开始

HTTP API

# 获取端口(Sanqian 启动时写入)
PORT=$(cat ~/.sanqian/runtime/api.port)

# 与默认 Agent 对话
curl -X POST "http://localhost:$PORT/api/agents/default/chat" \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "你好"}], "stream": false}'

SDK

npm install @yushaw/sanqian-sdk
import { SanqianSDK } from '@yushaw/sanqian-sdk'

const sdk = new SanqianSDK({
  appName: 'my-app',
  appVersion: '1.0.0',
  tools: [{
    name: 'greet',
    description: '向用户打招呼',
    parameters: {
      type: 'object',
      properties: { name: { type: 'string' } },
      required: ['name']
    },
    handler: async ({ name }) => `你好,${name}!`
  }]
})

// SDK 会在 Sanqian 启动时自动连接(通过监听 connection.json 文件变化)。
// 如果 Sanqian 已在运行,则立即连接。
// 如果 autoLaunchSanqian 为 true(默认),在 Sanqian 未运行时会自动启动它。

工具 (Tools)

工具是你的应用向 Sanqian 提供能力的主要方式。当用户提问时,AI Agent 可以调用你的工具来获取数据或执行操作。

定义工具

每个工具需要名称、描述(给 LLM 看的)、JSON Schema 参数和处理函数。

const sdk = new SanqianSDK({
  appName: 'my-notes-app',
  appVersion: '1.0.0',
  tools: [
    {
      name: 'search_notes',
      description: '按关键词搜索用户笔记',
      parameters: {
        type: 'object',
        properties: {
          query: { type: 'string', description: '搜索关键词' },
          limit: { type: 'number', description: '最大结果数', default: 10 }
        },
        required: ['query']
      },
      handler: async ({ query, limit }) => {
        const results = await db.searchNotes(query, limit)
        return results.map(n => ({ title: n.title, snippet: n.snippet }))
      }
    }
  ]
})

工具名称会自动加上应用前缀。search_notes 在 Sanqian 中变为 my-notes-app:search_notes

运行时更新工具

await sdk.updateTools([
  { name: 'search_notes', /* 更新后的定义 */ handler: newHandler },
  { name: 'new_tool', /* ... */ handler: newToolHandler }
])

这会替换整个工具列表。新的 Agent 运行会立即使用更新后的工具。

工具可搜索性

默认情况下,工具可以通过 Sanqian 的 search_capability 工具被发现。设置 searchable: false 隐藏工具但保持可用:

{
  name: 'internal_sync',
  description: '内部数据同步',
  searchable: false,  // 可用但不可被搜索发现
  parameters: { type: 'object' },
  handler: async () => { /* ... */ }
}

Agent

私有 Agent 是具有特定工具、技能和指令的自定义 AI 角色。它们会出现在 Sanqian 的 Agent 选择器中。

创建 Agent

const agent = await sdk.createAgent({
  agent_id: 'notes-assistant',
  name: '笔记助手',
  description: '帮助管理和搜索笔记',
  system_prompt: '你是一个笔记助手。帮助用户整理笔记。',
  tools: ['search_notes', 'create_note'],  // SDK 工具(自动加前缀)
  skills: ['web-research'],                 // Sanqian 内置技能
  subagents: ['*'],                         // 可以委派给任何 Agent
  searchable: true,                         // 可被其他 Agent 发现
})

工具名称格式

| 格式 | 示例 | 说明 | |------|------|------| | 短名称 | "search_notes" | 你的 SDK 工具(自动加应用前缀) | | 完整 SDK 名称 | "other-app:tool_name" | 其他 SDK 应用的工具 | | 内置 | "read_file", "run_bash_command" | Sanqian 内置工具 | | MCP | "mcp_servername_toolname" | MCP 服务器工具 | | 通配符 | ["*"] | 所有可用工具 |

子 Agent

控制你的 Agent 是否可以委派任务给其他 Agent:

{
  subagents: undefined,           // 不能使用 task 工具(默认)
  subagents: [],                  // 不能使用 task 工具(显式)
  subagents: ['*'],               // 可以调用任何 Agent
  subagents: ['agent1', 'agent2'] // 只能调用指定的 Agent
}

更新和删除

// 更新特定字段(其他不变)
await sdk.updateAgent('notes-assistant', {
  system_prompt: '更新后的指令...',
  tools: ['search_notes', 'create_note', 'delete_note']
})

// 删除
await sdk.deleteAgent('notes-assistant')

// 列出你的 Agent
const agents = await sdk.listAgents()

对话 (Chat)

向任何 Agent 发送消息并获取回复。支持流式和非流式模式。

非流式

const response = await sdk.chat('notes-assistant', [
  { role: 'user', content: '找到我关于 TypeScript 的笔记' }
])

console.log(response.message.content)

流式

for await (const event of sdk.chatStream('notes-assistant', [
  { role: 'user', content: '总结我最近的笔记' }
])) {
  switch (event.type) {
    case 'start':
      // 流开始,event.run_id 可用
      break
    case 'text':
      process.stdout.write(event.content || '')
      break
    case 'thinking':
      // 推理内容(DeepSeek R1、o3 等)
      break
    case 'tool_call':
      console.log(`调用工具: ${event.tool_call?.function.name}`)
      break
    case 'tool_args_chunk':
      // 流式工具参数(部分 JSON)
      break
    case 'tool_result':
      console.log(`结果: ${event.success ? '成功' : event.error}`)
      break
    case 'done':
      console.log(`\n会话: ${event.conversationId}`)
      break
    case 'error':
      console.error(event.error)
      break
  }
}

流事件

| 事件 | 字段 | 说明 | |------|------|------| | start | run_id, conversationId | 流开始 | | text | content | AI 文本片段 | | thinking | content | 推理内容(思考模型) | | tool_call | tool_call | 工具调用开始 | | tool_args_chunk | tool_call_id, tool_name, chunk | 流式工具参数 | | tool_args | tool_call_id, tool_name, args | 完整工具参数 | | tool_result | tool_call_id, result, success, error | 工具执行结果 | | interrupt | interrupt_type, interrupt_payload, run_id | HITL 暂停 | | done | conversationId, title | 流结束 | | error | error | 发生错误 | | cancelled | run_id | 运行被取消 |

有状态 vs 无状态

// 无状态:你自己管理消息历史
const r1 = await sdk.chat('agent', messages)
// r1.conversationId 是空字符串

// 有状态:服务器管理历史
const r1 = await sdk.chat('agent', messages, {
  persistHistory: true  // 创建服务端会话
})
// r1.conversationId 有值,后续使用它

const r2 = await sdk.chat('agent', [{ role: 'user', content: '继续' }], {
  conversationId: r1.conversationId
})

会话助手

多轮对话可以使用 Conversation 助手:

const conv = sdk.startConversation('notes-assistant')

const r1 = await conv.send('找到我的 TypeScript 笔记')
const r2 = await conv.send('总结第一个')
console.log(conv.id)  // 会话 ID(首次 send 后可用)

// 流式
for await (const event of conv.sendStream('还有更多细节吗?')) {
  if (event.type === 'text') process.stdout.write(event.content || '')
}

// 获取历史
const details = await conv.getDetails({ messageLimit: 50 })

// 删除
await conv.delete()

取消运行

let runId: string | undefined

for await (const event of sdk.chatStream('agent', messages)) {
  if (event.type === 'start') runId = event.run_id
  if (shouldCancel) {
    sdk.cancelRun(runId!)
    break
  }
}

自动发现

为对话启用自动发现技能、工具或子 Agent:

const response = await sdk.chat('agent', messages, {
  autoDiscoverSkills: true,   // Agent 可以搜索并使用技能
  autoDiscoverTools: true,    // Agent 可以搜索并使用工具
  autoDiscoverSubagents: true // Agent 可以委派给其他 Agent
})

本地文件导入

对于 Sanqian 后端与调用方在同一台机器、且后端能直接读取本地路径的场景,先调用 uploadFile(),再把返回的 file.path 传给 chat()

const uploaded = await sdk.uploadFile('/absolute/path/report.pdf', {
  conversationId,
  autoIndex: true,
  asyncProcess: true,
})

const response = await sdk.chat('agent', messages, {
  conversationId: uploaded.conversationId,
  filePaths: [uploaded.file.path],
})

uploadFile(localPath) 不是远程文件传输协议。对于远程 backend 或不同机器场景,仍应使用显式字节上传能力。


人机协作 (HITL)

HITL 允许 Agent 在运行中暂停并请求用户输入。三种中断类型:

| 类型 | 用途 | 示例 | |------|------|------| | approval_request | 执行前审批 | "删除文件 X?" -> 批准/拒绝 | | user_input_request | 请求用户输入 | "什么格式?" -> ["JSON", "CSV"] 或自由文本 | | user_action_request | 用户完成外部操作 | "需要登录" -> 用户登录 -> "完成" |

处理中断

for await (const event of sdk.chatStream('agent', messages)) {
  if (event.type === 'interrupt') {
    const { interrupt_type, interrupt_payload, run_id } = event

    if (interrupt_type === 'approval_request') {
      const approved = await showApprovalDialog(interrupt_payload)
      sdk.sendHitlResponse(run_id!, { approved })
    }

    if (interrupt_type === 'user_input_request') {
      const answer = await showInputDialog(interrupt_payload)
      sdk.sendHitlResponse(run_id!, { answer })
    }
  }
}

会话管理 (Conversations)

管理服务器上存储的对话历史。

// 列出会话
const { conversations, total } = await sdk.listConversations({
  agentId: 'notes-assistant',
  limit: 20,
  offset: 0
})

// 获取会话详情(含消息)
const detail = await sdk.getConversation(conversationId, {
  includeMessages: true,
  messageLimit: 50,
})

// 获取消息历史(通过 HTTP API,与主应用一致)
const history = await sdk.getMessages(conversationId, { limit: 50 })

// 删除会话
await sdk.deleteConversation(conversationId)

上下文提供者 (Context Providers)

上下文提供者让你的应用向对话注入动态状态。当用户附加上下文时,Sanqian 调用你的提供者获取最新数据。

三种提供者方法,均为可选:

| 方法 | 用途 | 调用时机 | |------|------|----------| | getCurrent() | 获取当前状态 | 用户发送带上下文的消息 | | getList(options) | 列出可用资源 | 用户打开 "+" 菜单浏览 | | getById(id) | 获取特定资源 | 用户从列表中选择 |

注册上下文提供者

const sdk = new SanqianSDK({
  appName: 'my-notes-app',
  appVersion: '1.0.0',
  tools: [/* ... */],
  contexts: [
    {
      id: 'active-note',
      name: '当前笔记',
      description: '正在编辑的笔记',
      getCurrent: async () => ({
        content: editor.getCurrentNote().content,
        title: editor.getCurrentNote().title,
        type: 'note',
      }),
    },
    {
      id: 'notes',
      name: '笔记库',
      description: '浏览并附加笔记',
      getList: async (options) => {
        const notes = await db.searchNotes(options?.query || '', {
          offset: options?.offset || 0,
          limit: options?.limit || 20,
        })
        return {
          items: notes.map(n => ({
            id: n.id,
            title: n.title,
            summary: n.snippet,
            type: 'note',
            group: n.folder,
          })),
          hasMore: notes.length === (options?.limit || 20),
        }
      },
      getById: async (id) => {
        const note = await db.getNote(id)
        if (!note) return null
        return { id: note.id, content: note.content, title: note.title, type: 'note' }
      },
    }
  ]
})

将上下文附加到 Agent

await sdk.createAgent({
  agent_id: 'notes-assistant',
  name: '笔记助手',
  tools: ['search_notes'],
  attached_contexts: ['active-note'],  // 自动加应用前缀
})

运行时更新上下文

await sdk.updateContexts([
  { id: 'active-note', name: '当前笔记', description: '...', getCurrent: newHandler },
])

会话资源 (Session Resources)

会话资源是你的应用推送给 Sanqian 的临时上下文。与上下文提供者(拉取式)不同,会话资源是推送式的。

// 推送资源
const stored = await sdk.pushResource({
  title: '当前笔记',
  content: '<note>\n# 我的笔记\n内容...\n</note>',
  summary: '我的笔记 - 2024-01-15',
})
console.log(stored.fullId) // "my-notes-app:abc123"

// 移除资源
await sdk.removeResource('my-notes-app:abc123')

// 清除所有资源
await sdk.clearResources()

// 本地缓存(无网络请求)
const resources = sdk.getSessionResources()

// 从服务器获取
const serverResources = await sdk.fetchSessionResources('notes-assistant')

在对话中附加会话资源

for await (const event of sdk.chatStream('agent', messages, {
  sessionResources: ['my-notes-app:abc123'],
})) { /* ... */ }

能力发现 (Capability Discovery)

查询 Sanqian 的完整能力注册表。

// 列出工具
const tools = await sdk.listTools()
const builtinTools = await sdk.listTools('builtin')

// 列出技能
const skills = await sdk.listSkills()

// 列出所有可用 Agent
const agents = await sdk.listAvailableAgents()

// 语义搜索(BM25 + 向量混合搜索)
const results = await sdk.searchCapabilities('文件操作', {
  type: 'tool',
  limit: 5,
})

Embedding 与 Rerank 配置

复用 Sanqian 中配置的嵌入和重排序模型。

const embedding = await sdk.getEmbeddingConfig()
if (embedding.available) {
  // 使用 embedding.apiUrl, embedding.apiKey, embedding.modelName
}

const rerank = await sdk.getRerankConfig()
if (rerank.available) {
  // 使用 rerank.apiUrl, rerank.apiKey, rerank.modelName
}

消息渠道 API

SDK 的 channels 方法通过 Sanqian HTTP 接口调用,并自动附带 X-App-Token 鉴权。它们只依赖运行时连接信息,不要求先完成 WebSocket 注册。

// 构建本地 HTTP 集成时可直接复用
const baseUrl = sdk.getBaseUrl() // 例如 http://127.0.0.1:8765

// 获取某个渠道的配置 schema(用于设置表单)
const schema = await sdk.getChannelConfigSchema('telegram')

// 创建账号前先探测配置是否可用
const probe = await sdk.probeChannelConfig({
  channel_type: 'telegram',
  config: { bot_token: process.env.TELEGRAM_BOT_TOKEN }
})
if (!probe.ok) {
  console.error(probe.error)
}

这里仅展示新增方法。完整渠道能力可通过 listChannelAccountscreateChannelAccountlistChannelBindingssendChannelMessage 等方法使用。


连接

连接生命周期

SDK 自动管理连接:

  1. 构造函数:读取 ~/.sanqian/runtime/connection.json 并监听变化
  2. 自动连接:connection.json 出现时(Sanqian 启动),SDK 自动连接
  3. 自动启动autoLaunchSanqian: true(默认),未运行时自动启动 Sanqian
  4. 注册:WebSocket 连接后,注册应用名称、工具和上下文提供者
  5. 心跳:30 秒间隔检测连接状态
  6. 自动重连:指数退避(500ms 到 5s)带抖动

重连控制

自动重连是引用计数的:

sdk.acquireReconnect()  // 聊天面板打开 - 请求持久连接
sdk.releaseReconnect()  // 聊天面板关闭 - 释放

连接状态

const state = sdk.getState()
// { connected, registering, registered, lastError?, reconnectAttempts }

sdk.isConnected() // true 当已连接且已注册

事件

sdk.on('connected', () => { /* WebSocket 已打开 */ })
sdk.on('disconnected', (reason) => { /* 连接断开 */ })
sdk.on('registered', () => { /* 工具和上下文已注册 */ })
sdk.on('error', (error) => { /* 连接错误 */ })
sdk.on('tool_call', ({ name, arguments }) => { /* 工具被调用 */ })
sdk.on('resourcePushed', (resource) => { /* 会话资源已推送 */ })
sdk.on('resourceRemoved', (resourceId) => { /* 用户移除了会话资源 */ })
sdk.on('resourcesCleared', (appName) => { /* 所有资源已清除 */ })

sdk.once('registered', () => { /* 一次性监听 */ })
sdk.removeAllListeners() // 移除所有监听器

SDKConfig 选项

const sdk = new SanqianSDK({
  // 必填
  appName: 'my-app',
  appVersion: '1.0.0',
  tools: [],

  // 显示
  displayName: 'My App',           // Sanqian UI 中显示的名称

  // 启动
  launchCommand: '/path/to/app',   // Sanqian 启动你的应用的命令
  metadata: { browser: 'chrome' }, // 应用元数据

  // 上下文
  contexts: [],                    // 上下文提供者

  // 超时
  reconnectInterval: 5000,         // 重连间隔(毫秒,默认 5000)
  heartbeatInterval: 30000,        // 心跳间隔(毫秒,默认 30000)
  toolExecutionTimeout: 30000,     // 工具超时(毫秒,默认 30000)

  // 自动启动
  autoLaunchSanqian: true,         // 未运行时启动 Sanqian(默认 true)
  sanqianPath: '/path/to/Sanqian', // 自定义可执行文件路径

  // 调试
  debug: false,                    // 控制台日志(默认 false)

  // 浏览器模式
  connectionInfo: undefined,       // 预配置连接信息(跳过文件发现)
})

浏览器构建 (Browser Build)

用于浏览器环境(扩展、Web 应用、Office 插件):

import { SanqianSDK } from '@yushaw/sanqian-sdk/browser'

const sdk = new SanqianSDK({
  appName: 'my-extension',
  appVersion: '1.0.0',
  connectionInfo: {
    port: 38765,
    token: 'your-token',
    ws_path: '/ws/apps',
    version: 1,
    pid: 0,
    started_at: '',
  },
  tools: [/* ... */]
})

await sdk.connect()

浏览器构建使用原生 WebSocket,需要提供 connectionInfo,不支持 autoLaunchSanqian 和 connection.json 文件监听。其他所有功能均可使用。


HTTP API 参考

简单集成无需 SDK。基础 URL:http://localhost:{PORT}(端口来自 ~/.sanqian/runtime/api.port)。

对话

POST /api/agents/{agent_id}/chat

| 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | messages | ChatMessage[] | 是 | 发送的消息 | | stream | boolean | 否 | 启用 SSE 流(默认 true) | | conversation_id | string | 否 | 继续已有会话 |

列出 Agent

GET /api/agents

示例

Python:

import requests, json

port = open('~/.sanqian/runtime/api.port').read().strip()

r = requests.post(f'http://localhost:{port}/api/agents/default/chat', json={
    'messages': [{'role': 'user', 'content': '你好'}],
    'stream': False,
})
print(r.json()['message']['content'])

JavaScript(无 SDK):

const port = require('fs').readFileSync(
  require('os').homedir() + '/.sanqian/runtime/api.port', 'utf8'
).trim()

const response = await fetch(`http://localhost:${port}/api/agents/default/chat`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    messages: [{ role: 'user', content: '你好' }],
    stream: false,
  }),
})
const data = await response.json()
console.log(data.message.content)

内置工具参考

文件操作

| 工具 | 说明 | |------|------| | read_file | 读取工作区文件 | | write_file | 写入工作区文件 | | edit_file | 精确字符串替换编辑文件 | | delete_file | 删除工作区文件 | | list_files | 列出目录内容 | | find_files | 按 glob 模式查找文件 | | search_file | 搜索文件内容 | | grep_content | 跨文件正则内容搜索 |

网络

| 工具 | 说明 | |------|------| | web_search | 网络搜索 | | fetch_web | 获取网页并转换为 Markdown |

执行

| 工具 | 说明 | |------|------| | run_bash_command | 执行 Shell 命令(沙箱化) |

记忆

| 工具 | 说明 | |------|------| | search_memory | 按语义相似度搜索记忆 | | save_memory | 保存新记忆 | | list_memories | 列出所有记忆 |

任务与 Agent

| 工具 | 说明 | |------|------| | todo_write | 创建和更新任务列表 | | task | 委派任务给其他 Agent | | search_capability | 搜索可用工具/技能/Agent | | ask_human | 需要更多信息时询问用户 |

视觉与图像

| 工具 | 说明 | |------|------| | vision_analyze | 图像分析 | | generate_image | 文本生成图像 |

macOS Apple 集成(仅 macOS)

| 工具 | 说明 | |------|------| | calendar_search / calendar_create / calendar_delete | 日历操作 | | notes_search / notes_create / notes_delete | 备忘录操作 | | reminders_search / reminders_create / reminders_complete / reminders_delete | 提醒事项操作 | | contacts_search | 搜索通讯录 |


常见问题

如何获取 agent_id? 使用 GET /api/agentssdk.listAgents()。常见内置 ID:defaultcoding

什么时候用 HTTP API vs SDK? HTTP API 适合任意语言的简单对话。SDK 适合需要工具、Agent、上下文提供者或会话资源的场景。

SDK 会话在 Sanqian 中可见吗? 是的。通过 SDK 创建的会话会出现在 Sanqian 的会话列表中。

Sanqian 重启后 Agent 会自动重连吗? 是的。Node.js SDK 监听 connection.json。Sanqian 重启写入新文件后,SDK 自动重连。

如果启动时 Sanqian 未运行怎么办? autoLaunchSanqian: true(默认)时,SDK 会在托盘模式下启动 Sanqian。否则 SDK 监听 connection.json,Sanqian 启动后自动连接。

错误码

| 码 | 说明 | |----|------| | 400 | 请求参数无效 | | 404 | Agent 或会话未找到 | | 429 | 频率限制 | | 500 | 内部服务器错误 |

Changelog

0.3.25

  • Centralized HTTP request pipeline: all HTTP API calls now go through a unified httpRequest() method that automatically injects X-App-Token auth headers and standardizes error handling via SanqianSDKError. This prevents auth header omissions and ensures consistent error types for SDK consumers.
  • Fixed 4 HTTP methods (getMessages, fetchSessionResources, listCapabilities, searchCapabilities) that were missing auth headers, causing 401 errors when ConnectionTokenMiddleware is active.
  • Net reduction of ~240 lines of duplicated fetch/error-handling boilerplate across Node.js and browser clients.
  • Fixed browser build (@yushaw/sanqian-sdk/browser) missing type exports: ConversationHistoryResult, ContextCapability, ResourceType, ResourceListOptions, ResourceListResult. Browser consumers can now import these types directly.
  • Fixed browser build missing Context Provider support: constructor registration, registration message, and incoming message handlers (context_get_current/list/by_id) now match the Node.js build.
  • Fixed browser build registration missing appKey field.
  • Unified error types: all throw new Error() in both clients replaced with createSDKError() for consistent SanqianSDKError across the SDK surface.
  • Added JSON parse error handling in httpRequest() to catch malformed server responses.
  • Fixed promise leak in connectWithInfo(): if WebSocket closes or errors before onopen fires, the connection promise now properly rejects instead of hanging indefinitely.
  • Added constructor config validation: appName required, tools must be array, each tool must have name, description, and handler.
  • Clamped timing config to sane minimums: reconnectInterval >= 100ms, heartbeatInterval >= 1000ms, toolExecutionTimeout >= 1000ms.
  • Fixed RegisterMessage.contexts type missing has_get_current/has_get_list/has_get_by_id fields (type definition now matches what the code actually sends).
  • Fixed SDKExtendedMethods.removeSessionResource -> removeResource to match the actual SDK method name. This was a real bug: packages/chat adapter was calling a non-existent method, causing session resource removal to silently no-op.
  • Fixed SDKExtendedMethods.getMessages return type to use ConversationHistoryResult instead of an anonymous inline type.
  • Added SDKExtendedMethods type export to browser build.
  • Fixed memory leak in tool execution timeout: Promise.race with setTimeout now properly clears the timer when the handler resolves first, preventing orphaned timers.
  • Fixed discovery file watcher debounce accumulation: rapid connection.json changes no longer stack up multiple pending timeouts. Previous timeouts are cancelled before creating new ones, and cleanup in stopWatching() also clears pending debounce timers.
  • Fixed colon truncation in tool/context-provider name parsing: name.split(":")[1] only returned text between 1st and 2nd colon. Changed to substring(indexOf(":") + 1) to preserve everything after the first colon (8 call sites across both clients).
  • Removed dead code: DiscoveryManager.buildWebSocketUrl() and buildHttpUrl() had zero production callers (clients have their own private versions that correctly support ws_protocol/ws_host). Also removed 4 no-op test mocks of these methods in client.test.ts.
  • SDK Client Deduplication Refactoring: Extracted ~2100 lines of identical code from client.ts and client.browser.ts into client.base.ts (SanqianSDKBase abstract class). Platform-specific behavior is handled via 5 abstract methods + 1 abstract getter. client.ts (Node.js) is now ~250 lines, client.browser.ts (Browser) is ~80 lines. Zero public API changes. Fixed 3 drift bugs in the process:
    • updateTools() in browser was missing this.config.tools = tools (reconnect used stale tool list)
    • disconnect() in browser was missing sessionResources.clear() (stale resource cache on reconnect)
    • listAvailableAgents() in browser used scope: "available" vs Node's scope: "enabled" (unified to "enabled")