@yushaw/sanqian-sdk 0.3.14 → 0.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1781 -0
- package/dist/index.browser.d.mts +26 -4
- package/dist/index.browser.mjs +15 -11
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +24 -4
- package/dist/index.d.ts +24 -4
- package/dist/index.js +13 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +15 -51
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/README.md
ADDED
|
@@ -0,0 +1,1781 @@
|
|
|
1
|
+
# Sanqian Developer Guide / 三千开发者指南
|
|
2
|
+
|
|
3
|
+
[English](#english) | [中文](#中文)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<a name="english"></a>
|
|
8
|
+
|
|
9
|
+
# English
|
|
10
|
+
|
|
11
|
+
Sanqian provides two ways to integrate with external applications:
|
|
12
|
+
|
|
13
|
+
| Method | Use Case | Protocol |
|
|
14
|
+
|--------|----------|----------|
|
|
15
|
+
| **HTTP API** | Simple chat, any language | REST + SSE |
|
|
16
|
+
| **SDK** | Tool registration, agents, context injection, deep integration | WebSocket |
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### HTTP API
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Get port (Sanqian writes it on startup)
|
|
24
|
+
PORT=$(cat ~/.sanqian/runtime/api.port)
|
|
25
|
+
|
|
26
|
+
# Chat with default agent
|
|
27
|
+
curl -X POST "http://localhost:$PORT/api/agents/default/chat" \
|
|
28
|
+
-H "Content-Type: application/json" \
|
|
29
|
+
-d '{"messages": [{"role": "user", "content": "Hello"}], "stream": false}'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### SDK
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @yushaw/sanqian-sdk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { SanqianSDK } from '@yushaw/sanqian-sdk'
|
|
40
|
+
|
|
41
|
+
const sdk = new SanqianSDK({
|
|
42
|
+
appName: 'my-app',
|
|
43
|
+
appVersion: '1.0.0',
|
|
44
|
+
tools: [{
|
|
45
|
+
name: 'greet',
|
|
46
|
+
description: 'Greet a user',
|
|
47
|
+
parameters: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: { name: { type: 'string' } },
|
|
50
|
+
required: ['name']
|
|
51
|
+
},
|
|
52
|
+
handler: async ({ name }) => `Hello, ${name}!`
|
|
53
|
+
}]
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// SDK auto-connects when Sanqian starts (via connection.json file watching).
|
|
57
|
+
// If Sanqian is already running, it connects immediately.
|
|
58
|
+
// If autoLaunchSanqian is true (default), it launches Sanqian if not running.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Tools
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
### Define Tools
|
|
68
|
+
|
|
69
|
+
Each tool needs a name, description (for the LLM), JSON Schema parameters, and a handler function.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const sdk = new SanqianSDK({
|
|
73
|
+
appName: 'my-notes-app',
|
|
74
|
+
appVersion: '1.0.0',
|
|
75
|
+
tools: [
|
|
76
|
+
{
|
|
77
|
+
name: 'search_notes',
|
|
78
|
+
description: 'Search through user notes by keyword',
|
|
79
|
+
parameters: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
query: { type: 'string', description: 'Search keyword' },
|
|
83
|
+
limit: { type: 'number', description: 'Max results', default: 10 }
|
|
84
|
+
},
|
|
85
|
+
required: ['query']
|
|
86
|
+
},
|
|
87
|
+
handler: async ({ query, limit }) => {
|
|
88
|
+
const results = await db.searchNotes(query, limit)
|
|
89
|
+
return results.map(n => ({ title: n.title, snippet: n.snippet }))
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'create_note',
|
|
94
|
+
description: 'Create a new note',
|
|
95
|
+
parameters: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
title: { type: 'string' },
|
|
99
|
+
content: { type: 'string' }
|
|
100
|
+
},
|
|
101
|
+
required: ['title', 'content']
|
|
102
|
+
},
|
|
103
|
+
handler: async ({ title, content }) => {
|
|
104
|
+
const note = await db.createNote(title, content)
|
|
105
|
+
return { id: note.id, title: note.title }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Tool names are automatically prefixed with your app name. `search_notes` becomes `my-notes-app:search_notes` in Sanqian.
|
|
113
|
+
|
|
114
|
+
### Update Tools at Runtime
|
|
115
|
+
|
|
116
|
+
You can add, remove, or modify tools after initialization:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
await sdk.updateTools([
|
|
120
|
+
{ name: 'search_notes', /* updated definition */ handler: newHandler },
|
|
121
|
+
{ name: 'new_tool', /* ... */ handler: newToolHandler }
|
|
122
|
+
])
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
This replaces the entire tool list. The change takes effect immediately for new agent runs.
|
|
126
|
+
|
|
127
|
+
### Tool Searchability
|
|
128
|
+
|
|
129
|
+
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:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
{
|
|
133
|
+
name: 'internal_sync',
|
|
134
|
+
description: 'Internal data sync',
|
|
135
|
+
searchable: false, // Available but not discoverable via search
|
|
136
|
+
parameters: { type: 'object' },
|
|
137
|
+
handler: async () => { /* ... */ }
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Agents
|
|
144
|
+
|
|
145
|
+
Private agents are custom AI personas with specific tools, skills, and instructions. They appear in Sanqian's agent picker.
|
|
146
|
+
|
|
147
|
+
### Create an Agent
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const agent = await sdk.createAgent({
|
|
151
|
+
agent_id: 'notes-assistant',
|
|
152
|
+
name: 'Notes Assistant',
|
|
153
|
+
description: 'Helps manage and search notes',
|
|
154
|
+
system_prompt: 'You are a notes assistant. Help users organize their notes.',
|
|
155
|
+
tools: ['search_notes', 'create_note'], // Your SDK tools (auto-prefixed)
|
|
156
|
+
skills: ['web-research'], // Sanqian built-in skills
|
|
157
|
+
subagents: ['*'], // Can delegate to any agent
|
|
158
|
+
searchable: true, // Discoverable by other agents
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
console.log(agent.agent_id) // "my-notes-app:notes-assistant"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Tool Name Formats
|
|
165
|
+
|
|
166
|
+
The `tools` array in agent config supports multiple formats:
|
|
167
|
+
|
|
168
|
+
| Format | Example | Description |
|
|
169
|
+
|--------|---------|-------------|
|
|
170
|
+
| Short name | `"search_notes"` | Your SDK tool (auto-prefixed with app name) |
|
|
171
|
+
| Full SDK name | `"other-app:tool_name"` | Another SDK app's tool |
|
|
172
|
+
| Built-in | `"read_file"`, `"run_bash_command"` | Sanqian built-in tools |
|
|
173
|
+
| MCP | `"mcp_servername_toolname"` | MCP server tools |
|
|
174
|
+
| Wildcard | `["*"]` | All available tools |
|
|
175
|
+
|
|
176
|
+
### Sub-agents
|
|
177
|
+
|
|
178
|
+
Control whether your agent can delegate tasks to other agents:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
{
|
|
182
|
+
subagents: undefined, // Cannot use task tool (default)
|
|
183
|
+
subagents: [], // Cannot use task tool (explicit)
|
|
184
|
+
subagents: ['*'], // Can call any agent
|
|
185
|
+
subagents: ['agent1', 'agent2'] // Can only call specific agents
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Update and Delete
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// Update specific fields (others unchanged)
|
|
193
|
+
await sdk.updateAgent('notes-assistant', {
|
|
194
|
+
system_prompt: 'Updated instructions...',
|
|
195
|
+
tools: ['search_notes', 'create_note', 'delete_note']
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// Delete
|
|
199
|
+
await sdk.deleteAgent('notes-assistant')
|
|
200
|
+
|
|
201
|
+
// List your agents
|
|
202
|
+
const agents = await sdk.listAgents()
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Chat
|
|
208
|
+
|
|
209
|
+
Send messages to any agent and get responses. Supports both streaming and non-streaming modes.
|
|
210
|
+
|
|
211
|
+
### Non-streaming
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
const response = await sdk.chat('notes-assistant', [
|
|
215
|
+
{ role: 'user', content: 'Find my notes about TypeScript' }
|
|
216
|
+
])
|
|
217
|
+
|
|
218
|
+
console.log(response.message.content)
|
|
219
|
+
console.log(response.conversationId) // Empty string if stateless
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Streaming
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
for await (const event of sdk.chatStream('notes-assistant', [
|
|
226
|
+
{ role: 'user', content: 'Summarize my recent notes' }
|
|
227
|
+
])) {
|
|
228
|
+
switch (event.type) {
|
|
229
|
+
case 'start':
|
|
230
|
+
// Stream started, event.run_id available
|
|
231
|
+
break
|
|
232
|
+
case 'text':
|
|
233
|
+
process.stdout.write(event.content || '')
|
|
234
|
+
break
|
|
235
|
+
case 'thinking':
|
|
236
|
+
// Reasoning content (DeepSeek R1, o3, etc.)
|
|
237
|
+
break
|
|
238
|
+
case 'tool_call':
|
|
239
|
+
console.log(`Calling: ${event.tool_call?.function.name}`)
|
|
240
|
+
break
|
|
241
|
+
case 'tool_args_chunk':
|
|
242
|
+
// Streaming tool arguments (partial JSON)
|
|
243
|
+
break
|
|
244
|
+
case 'tool_result':
|
|
245
|
+
console.log(`Result: ${event.success ? 'ok' : event.error}`)
|
|
246
|
+
break
|
|
247
|
+
case 'done':
|
|
248
|
+
console.log(`\nConversation: ${event.conversationId}`)
|
|
249
|
+
break
|
|
250
|
+
case 'error':
|
|
251
|
+
console.error(event.error)
|
|
252
|
+
break
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Stream Events
|
|
258
|
+
|
|
259
|
+
| Event | Fields | Description |
|
|
260
|
+
|-------|--------|-------------|
|
|
261
|
+
| `start` | `run_id`, `conversationId` | Stream started |
|
|
262
|
+
| `text` | `content` | Text chunk from the AI |
|
|
263
|
+
| `thinking` | `content` | Reasoning content (thinking models) |
|
|
264
|
+
| `tool_call` | `tool_call` | Tool invocation started |
|
|
265
|
+
| `tool_args_chunk` | `tool_call_id`, `tool_name`, `chunk` | Streaming tool arguments |
|
|
266
|
+
| `tool_args` | `tool_call_id`, `tool_name`, `args` | Complete tool arguments |
|
|
267
|
+
| `tool_result` | `tool_call_id`, `result`, `success`, `error` | Tool execution result |
|
|
268
|
+
| `interrupt` | `interrupt_type`, `interrupt_payload`, `run_id` | HITL pause (see below) |
|
|
269
|
+
| `done` | `conversationId`, `title` | Stream finished |
|
|
270
|
+
| `error` | `error` | Error occurred |
|
|
271
|
+
| `cancelled` | `run_id` | Run was cancelled |
|
|
272
|
+
|
|
273
|
+
### Stateful vs Stateless
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// Stateless: you manage message history
|
|
277
|
+
const r1 = await sdk.chat('agent', messages)
|
|
278
|
+
// r1.conversationId is empty string
|
|
279
|
+
|
|
280
|
+
// Stateful: server manages history
|
|
281
|
+
const r1 = await sdk.chat('agent', messages, {
|
|
282
|
+
persistHistory: true // Creates a server-side conversation
|
|
283
|
+
})
|
|
284
|
+
// r1.conversationId is set, use it for follow-ups
|
|
285
|
+
|
|
286
|
+
const r2 = await sdk.chat('agent', [{ role: 'user', content: 'Follow up' }], {
|
|
287
|
+
conversationId: r1.conversationId
|
|
288
|
+
})
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Conversation Helper
|
|
292
|
+
|
|
293
|
+
For multi-turn conversations, use the `Conversation` helper:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
const conv = sdk.startConversation('notes-assistant')
|
|
297
|
+
|
|
298
|
+
const r1 = await conv.send('Find my TypeScript notes')
|
|
299
|
+
console.log(r1.message.content)
|
|
300
|
+
|
|
301
|
+
const r2 = await conv.send('Summarize the first one')
|
|
302
|
+
console.log(conv.id) // Conversation ID (available after first send)
|
|
303
|
+
|
|
304
|
+
// Streaming
|
|
305
|
+
for await (const event of conv.sendStream('Any more details?')) {
|
|
306
|
+
if (event.type === 'text') process.stdout.write(event.content || '')
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Get history
|
|
310
|
+
const details = await conv.getDetails({ messageLimit: 50 })
|
|
311
|
+
|
|
312
|
+
// Delete
|
|
313
|
+
await conv.delete()
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Cancel a Run
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
let runId: string | undefined
|
|
320
|
+
|
|
321
|
+
for await (const event of sdk.chatStream('agent', messages)) {
|
|
322
|
+
if (event.type === 'start') {
|
|
323
|
+
runId = event.run_id
|
|
324
|
+
}
|
|
325
|
+
if (shouldCancel) {
|
|
326
|
+
sdk.cancelRun(runId!)
|
|
327
|
+
break
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Auto-discovery
|
|
333
|
+
|
|
334
|
+
Enable automatic discovery of skills, tools, or sub-agents for a chat call:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
const response = await sdk.chat('agent', messages, {
|
|
338
|
+
autoDiscoverSkills: true, // Agent can find and use skills
|
|
339
|
+
autoDiscoverTools: true, // Agent can find and use tools
|
|
340
|
+
autoDiscoverSubagents: true // Agent can delegate to other agents
|
|
341
|
+
})
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Human-in-the-Loop (HITL)
|
|
347
|
+
|
|
348
|
+
HITL allows the agent to pause and ask for user input during a run. There are three interrupt types:
|
|
349
|
+
|
|
350
|
+
| Type | Use Case | Example |
|
|
351
|
+
|------|----------|---------|
|
|
352
|
+
| `approval_request` | Pre-execution approval | "Delete file X?" -> Approve/Reject |
|
|
353
|
+
| `user_input_request` | Ask for user input | "What format?" -> ["JSON", "CSV"] or free text |
|
|
354
|
+
| `user_action_request` | User completes external action | "Login required" -> User logs in -> "Done" |
|
|
355
|
+
|
|
356
|
+
### Handling Interrupts
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
for await (const event of sdk.chatStream('agent', messages)) {
|
|
360
|
+
if (event.type === 'interrupt') {
|
|
361
|
+
const { interrupt_type, interrupt_payload, run_id } = event
|
|
362
|
+
|
|
363
|
+
if (interrupt_type === 'approval_request') {
|
|
364
|
+
const approved = await showApprovalDialog(interrupt_payload)
|
|
365
|
+
sdk.sendHitlResponse(run_id!, { approved })
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (interrupt_type === 'user_input_request') {
|
|
369
|
+
const answer = await showInputDialog(interrupt_payload)
|
|
370
|
+
sdk.sendHitlResponse(run_id!, { answer })
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (interrupt_type === 'user_action_request') {
|
|
374
|
+
await waitForUserAction(interrupt_payload)
|
|
375
|
+
sdk.sendHitlResponse(run_id!, { approved: true })
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### HITL Response Options
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
sdk.sendHitlResponse(runId, {
|
|
385
|
+
approved: true, // For approval_request
|
|
386
|
+
remember: true, // Remember this choice for future
|
|
387
|
+
answer: 'JSON', // For user_input_request (text)
|
|
388
|
+
selected_indices: [0, 2], // For multi-select options
|
|
389
|
+
cancelled: true, // User cancelled
|
|
390
|
+
timed_out: true, // Request timed out
|
|
391
|
+
})
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Conversations
|
|
397
|
+
|
|
398
|
+
Manage conversation history stored on the server.
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// List conversations (optionally filter by agent)
|
|
402
|
+
const { conversations, total } = await sdk.listConversations({
|
|
403
|
+
agentId: 'notes-assistant',
|
|
404
|
+
limit: 20,
|
|
405
|
+
offset: 0
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
// Get conversation with messages
|
|
409
|
+
const detail = await sdk.getConversation(conversationId, {
|
|
410
|
+
includeMessages: true,
|
|
411
|
+
messageLimit: 50,
|
|
412
|
+
messageOffset: 0
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
// Get message history (via HTTP API, aligned with main app)
|
|
416
|
+
const history = await sdk.getMessages(conversationId, {
|
|
417
|
+
limit: 50,
|
|
418
|
+
offset: 0
|
|
419
|
+
})
|
|
420
|
+
// Returns: { messages, has_more, session_id, returned_turns }
|
|
421
|
+
|
|
422
|
+
// Delete a conversation
|
|
423
|
+
await sdk.deleteConversation(conversationId)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## Context Providers
|
|
429
|
+
|
|
430
|
+
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.
|
|
431
|
+
|
|
432
|
+
Three provider methods, all optional:
|
|
433
|
+
|
|
434
|
+
| Method | Purpose | When Called |
|
|
435
|
+
|--------|---------|------------|
|
|
436
|
+
| `getCurrent()` | Get current state | User sends message with context attached |
|
|
437
|
+
| `getList(options)` | List available resources | User opens "+" menu to browse |
|
|
438
|
+
| `getById(id)` | Get specific resource | User selects item from list |
|
|
439
|
+
|
|
440
|
+
### Register Context Providers
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
const sdk = new SanqianSDK({
|
|
444
|
+
appName: 'my-notes-app',
|
|
445
|
+
appVersion: '1.0.0',
|
|
446
|
+
tools: [/* ... */],
|
|
447
|
+
contexts: [
|
|
448
|
+
{
|
|
449
|
+
id: 'active-note',
|
|
450
|
+
name: 'Active Note',
|
|
451
|
+
description: 'The note currently being edited',
|
|
452
|
+
getCurrent: async () => ({
|
|
453
|
+
content: editor.getCurrentNote().content,
|
|
454
|
+
title: editor.getCurrentNote().title,
|
|
455
|
+
type: 'note',
|
|
456
|
+
}),
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
id: 'notes',
|
|
460
|
+
name: 'Notes Library',
|
|
461
|
+
description: 'Browse and attach notes',
|
|
462
|
+
getList: async (options) => {
|
|
463
|
+
const notes = await db.searchNotes(options?.query || '', {
|
|
464
|
+
offset: options?.offset || 0,
|
|
465
|
+
limit: options?.limit || 20,
|
|
466
|
+
})
|
|
467
|
+
return {
|
|
468
|
+
items: notes.map(n => ({
|
|
469
|
+
id: n.id,
|
|
470
|
+
title: n.title,
|
|
471
|
+
summary: n.snippet,
|
|
472
|
+
type: 'note',
|
|
473
|
+
group: n.folder,
|
|
474
|
+
})),
|
|
475
|
+
hasMore: notes.length === (options?.limit || 20),
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
getById: async (id) => {
|
|
479
|
+
const note = await db.getNote(id)
|
|
480
|
+
if (!note) return null
|
|
481
|
+
return {
|
|
482
|
+
id: note.id,
|
|
483
|
+
content: note.content,
|
|
484
|
+
title: note.title,
|
|
485
|
+
type: 'note',
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
}
|
|
489
|
+
]
|
|
490
|
+
})
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Attach Contexts to Agents
|
|
494
|
+
|
|
495
|
+
Auto-attach context providers to an agent so they're always included:
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
await sdk.createAgent({
|
|
499
|
+
agent_id: 'notes-assistant',
|
|
500
|
+
name: 'Notes Assistant',
|
|
501
|
+
tools: ['search_notes'],
|
|
502
|
+
attached_contexts: ['active-note'], // Auto-prefixed with app name
|
|
503
|
+
})
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Update Contexts at Runtime
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
await sdk.updateContexts([
|
|
510
|
+
{ id: 'active-note', name: 'Active Note', description: '...', getCurrent: newHandler },
|
|
511
|
+
{ id: 'new-context', name: 'New Context', description: '...', getList: listHandler },
|
|
512
|
+
])
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Context Data Format
|
|
516
|
+
|
|
517
|
+
The `ContextData` object returned by providers:
|
|
518
|
+
|
|
519
|
+
| Field | Required | Description |
|
|
520
|
+
|-------|----------|-------------|
|
|
521
|
+
| `content` | Yes | Content injected into conversation |
|
|
522
|
+
| `id` | No | Resource ID (for `getById` scenario) |
|
|
523
|
+
| `title` | No | Display title |
|
|
524
|
+
| `summary` | No | UI preview text |
|
|
525
|
+
| `version` | No | Change detection (defaults to content hash) |
|
|
526
|
+
| `type` | No | Resource type for display styling |
|
|
527
|
+
| `metadata` | No | Extra data accessible in templates |
|
|
528
|
+
| `template` | No | Custom Mustache template: `"# {{title}}\n\n{{content}}"` |
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Session Resources
|
|
533
|
+
|
|
534
|
+
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.
|
|
535
|
+
|
|
536
|
+
They're visible in all Chat UI instances and persist until your app disconnects or removes them.
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// Push a resource
|
|
540
|
+
const stored = await sdk.pushResource({
|
|
541
|
+
title: 'Current Note',
|
|
542
|
+
content: '<note>\n# My Note\nContent here...\n</note>',
|
|
543
|
+
summary: 'My Note - 2024-01-15',
|
|
544
|
+
icon: '📝',
|
|
545
|
+
type: 'note',
|
|
546
|
+
})
|
|
547
|
+
console.log(stored.fullId) // "my-notes-app:abc123"
|
|
548
|
+
|
|
549
|
+
// Remove a specific resource
|
|
550
|
+
await sdk.removeResource('my-notes-app:abc123')
|
|
551
|
+
|
|
552
|
+
// Clear all your app's resources
|
|
553
|
+
await sdk.clearResources()
|
|
554
|
+
|
|
555
|
+
// Get local cache (no server round-trip)
|
|
556
|
+
const resources = sdk.getSessionResources()
|
|
557
|
+
|
|
558
|
+
// Fetch from server (optionally filtered by agent)
|
|
559
|
+
const serverResources = await sdk.fetchSessionResources('notes-assistant')
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Attach Session Resources to Chat
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
for await (const event of sdk.chatStream('agent', messages, {
|
|
566
|
+
sessionResources: ['my-notes-app:abc123'], // Include specific resources
|
|
567
|
+
})) {
|
|
568
|
+
// ...
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### Listen for Removal
|
|
573
|
+
|
|
574
|
+
When a user removes a session resource from the Chat UI:
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
sdk.on('resourceRemoved', (resourceId) => {
|
|
578
|
+
console.log(`User removed: ${resourceId}`)
|
|
579
|
+
})
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## Capability Discovery
|
|
585
|
+
|
|
586
|
+
Query Sanqian's full capability registry: tools, skills, agents, and context providers.
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
// List all tools
|
|
590
|
+
const tools = await sdk.listTools()
|
|
591
|
+
|
|
592
|
+
// List by source
|
|
593
|
+
const builtinTools = await sdk.listTools('builtin')
|
|
594
|
+
const sdkTools = await sdk.listTools('sdk')
|
|
595
|
+
const mcpTools = await sdk.listTools('mcp')
|
|
596
|
+
|
|
597
|
+
// List skills
|
|
598
|
+
const skills = await sdk.listSkills()
|
|
599
|
+
|
|
600
|
+
// List all available agents (not just your own)
|
|
601
|
+
const agents = await sdk.listAvailableAgents()
|
|
602
|
+
|
|
603
|
+
// Generic listing with filters
|
|
604
|
+
const caps = await sdk.listCapabilities({
|
|
605
|
+
type: 'tool', // 'tool' | 'skill' | 'agent' | 'context' | 'all'
|
|
606
|
+
source: 'builtin', // Filter by source
|
|
607
|
+
category: 'file', // Filter by category (tools only)
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
// Semantic search (BM25 + Vector hybrid)
|
|
611
|
+
const results = await sdk.searchCapabilities('file operations', {
|
|
612
|
+
type: 'tool',
|
|
613
|
+
limit: 5,
|
|
614
|
+
})
|
|
615
|
+
for (const r of results) {
|
|
616
|
+
console.log(`${r.capability.id}: score=${r.score}`)
|
|
617
|
+
console.log(` How to use: ${r.howToUse}`)
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## Embedding & Rerank Config
|
|
624
|
+
|
|
625
|
+
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.
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
// Embedding
|
|
629
|
+
const embedding = await sdk.getEmbeddingConfig()
|
|
630
|
+
if (embedding.available) {
|
|
631
|
+
// Use embedding.apiUrl, embedding.apiKey, embedding.modelName, embedding.dimensions
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Rerank
|
|
635
|
+
const rerank = await sdk.getRerankConfig()
|
|
636
|
+
if (rerank.available) {
|
|
637
|
+
// Use rerank.apiUrl, rerank.apiKey, rerank.modelName
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
## Connection
|
|
644
|
+
|
|
645
|
+
### Connection Lifecycle
|
|
646
|
+
|
|
647
|
+
The SDK manages connection automatically:
|
|
648
|
+
|
|
649
|
+
1. **Constructor**: Reads `~/.sanqian/runtime/connection.json` and watches for changes
|
|
650
|
+
2. **Auto-connect**: When connection.json appears (Sanqian starts), SDK connects automatically
|
|
651
|
+
3. **Auto-launch**: If `autoLaunchSanqian: true` (default), SDK starts Sanqian if not running
|
|
652
|
+
4. **Registration**: After WebSocket connects, SDK registers app name, tools, and context providers
|
|
653
|
+
5. **Heartbeat**: 30-second interval to detect dead connections
|
|
654
|
+
6. **Auto-reconnect**: Exponential backoff (500ms to 5s) with jitter when connection drops
|
|
655
|
+
|
|
656
|
+
```
|
|
657
|
+
connection.json appears -> WebSocket connect -> Register -> Heartbeat
|
|
658
|
+
^ |
|
|
659
|
+
| Reconnect (backoff) v
|
|
660
|
+
+<------------------ Disconnect <-------- Heartbeat timeout
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### Reconnect Control
|
|
664
|
+
|
|
665
|
+
Auto-reconnect is reference-counted. Enable it when your UI needs a persistent connection:
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
// Chat panel opens - request persistent connection
|
|
669
|
+
sdk.acquireReconnect()
|
|
670
|
+
|
|
671
|
+
// Chat panel closes - release
|
|
672
|
+
sdk.releaseReconnect()
|
|
673
|
+
|
|
674
|
+
// When refCount reaches 0, auto-reconnect stops
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Connection State
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
const state = sdk.getState()
|
|
681
|
+
// { connected: boolean, registering: boolean, registered: boolean,
|
|
682
|
+
// lastError?: Error, reconnectAttempts: number }
|
|
683
|
+
|
|
684
|
+
sdk.isConnected() // true when connected AND registered
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### Events
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
sdk.on('connected', () => { /* WebSocket opened */ })
|
|
691
|
+
sdk.on('disconnected', (reason) => { /* Connection lost */ })
|
|
692
|
+
sdk.on('registered', () => { /* Tools and contexts registered */ })
|
|
693
|
+
sdk.on('error', (error) => { /* Connection error */ })
|
|
694
|
+
sdk.on('tool_call', ({ name, arguments }) => { /* Tool invoked (for logging) */ })
|
|
695
|
+
sdk.on('resourcePushed', (resource) => { /* Session resource pushed */ })
|
|
696
|
+
sdk.on('resourceRemoved', (resourceId) => { /* Session resource removed by user */ })
|
|
697
|
+
sdk.on('resourcesCleared', (appName) => { /* All resources cleared */ })
|
|
698
|
+
|
|
699
|
+
// One-time listener
|
|
700
|
+
sdk.once('registered', () => { /* ... */ })
|
|
701
|
+
|
|
702
|
+
// Remove listeners
|
|
703
|
+
sdk.off('connected', handler)
|
|
704
|
+
sdk.removeAllListeners() // All events
|
|
705
|
+
sdk.removeAllListeners('error') // Specific event
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### Manual Connection
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
// Connect (usually not needed - SDK auto-connects)
|
|
712
|
+
await sdk.connect()
|
|
713
|
+
|
|
714
|
+
// Disconnect
|
|
715
|
+
await sdk.disconnect()
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Launched by Sanqian
|
|
719
|
+
|
|
720
|
+
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).
|
|
721
|
+
|
|
722
|
+
### SDKConfig Options
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
const sdk = new SanqianSDK({
|
|
726
|
+
// Required
|
|
727
|
+
appName: 'my-app',
|
|
728
|
+
appVersion: '1.0.0',
|
|
729
|
+
tools: [],
|
|
730
|
+
|
|
731
|
+
// Display
|
|
732
|
+
displayName: 'My App', // Shown in Sanqian UI
|
|
733
|
+
|
|
734
|
+
// Launch
|
|
735
|
+
launchCommand: '/path/to/app', // For Sanqian to start your app
|
|
736
|
+
metadata: { browser: 'chrome' }, // App metadata stored with registration
|
|
737
|
+
|
|
738
|
+
// Context
|
|
739
|
+
contexts: [], // Context providers (see above)
|
|
740
|
+
|
|
741
|
+
// Timeouts
|
|
742
|
+
reconnectInterval: 5000, // Reconnect interval (ms, default: 5000)
|
|
743
|
+
heartbeatInterval: 30000, // Heartbeat interval (ms, default: 30000)
|
|
744
|
+
toolExecutionTimeout: 30000, // Tool timeout (ms, default: 30000)
|
|
745
|
+
|
|
746
|
+
// Auto-launch
|
|
747
|
+
autoLaunchSanqian: true, // Launch Sanqian if not running (default: true)
|
|
748
|
+
sanqianPath: '/path/to/Sanqian', // Custom executable path (optional)
|
|
749
|
+
|
|
750
|
+
// Debug
|
|
751
|
+
debug: false, // Console logging (default: false)
|
|
752
|
+
|
|
753
|
+
// Browser mode (see Browser Build section)
|
|
754
|
+
connectionInfo: undefined, // Pre-configured connection (skips file discovery)
|
|
755
|
+
})
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
## Browser Build
|
|
761
|
+
|
|
762
|
+
For browser environments (extensions, web apps, Office Add-ins), use the browser-specific import:
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
import { SanqianSDK } from '@yushaw/sanqian-sdk/browser'
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
The browser build:
|
|
769
|
+
- Uses native `WebSocket` (no Node.js `ws` dependency)
|
|
770
|
+
- Requires `connectionInfo` in config (no filesystem access for discovery)
|
|
771
|
+
- Does not support `autoLaunchSanqian` or connection.json file watching
|
|
772
|
+
- Supports all other features: tools, agents, chat, context providers, session resources, HITL, capability discovery
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
import { SanqianSDK } from '@yushaw/sanqian-sdk/browser'
|
|
776
|
+
|
|
777
|
+
const sdk = new SanqianSDK({
|
|
778
|
+
appName: 'my-extension',
|
|
779
|
+
appVersion: '1.0.0',
|
|
780
|
+
connectionInfo: {
|
|
781
|
+
port: 38765,
|
|
782
|
+
token: 'your-token',
|
|
783
|
+
ws_path: '/ws/apps',
|
|
784
|
+
version: 1,
|
|
785
|
+
pid: 0,
|
|
786
|
+
started_at: '',
|
|
787
|
+
},
|
|
788
|
+
tools: [/* ... */]
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
await sdk.connect()
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
## HTTP API Reference
|
|
797
|
+
|
|
798
|
+
For simple integrations without the SDK. Base URL: `http://localhost:{PORT}` (port from `~/.sanqian/runtime/api.port`).
|
|
799
|
+
|
|
800
|
+
### Chat
|
|
801
|
+
|
|
802
|
+
```
|
|
803
|
+
POST /api/agents/{agent_id}/chat
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
**Request body:**
|
|
807
|
+
|
|
808
|
+
| Field | Type | Required | Description |
|
|
809
|
+
|-------|------|----------|-------------|
|
|
810
|
+
| `messages` | `ChatMessage[]` | Yes | Messages to send |
|
|
811
|
+
| `stream` | `boolean` | No | Enable SSE streaming (default: true) |
|
|
812
|
+
| `conversation_id` | `string` | No | Continue existing conversation |
|
|
813
|
+
|
|
814
|
+
**Non-streaming response (`stream: false`):**
|
|
815
|
+
```json
|
|
816
|
+
{
|
|
817
|
+
"message": { "role": "assistant", "content": "..." },
|
|
818
|
+
"conversation_id": "abc123"
|
|
819
|
+
}
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
**Streaming response (`stream: true`, SSE):**
|
|
823
|
+
```
|
|
824
|
+
data: {"type": "text", "content": "Hello"}
|
|
825
|
+
data: {"type": "text", "content": " world"}
|
|
826
|
+
data: {"type": "tool_call", "tool_call": {...}}
|
|
827
|
+
data: {"type": "tool_result", "tool_result": {...}}
|
|
828
|
+
data: {"type": "done", "conversation_id": "abc123"}
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### List Agents
|
|
832
|
+
|
|
833
|
+
```
|
|
834
|
+
GET /api/agents
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
Returns all available agents:
|
|
838
|
+
```json
|
|
839
|
+
[
|
|
840
|
+
{ "id": "default", "name": "Default", "description": "..." },
|
|
841
|
+
{ "id": "coding", "name": "Coding", "description": "..." }
|
|
842
|
+
]
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
### Get Agent
|
|
846
|
+
|
|
847
|
+
```
|
|
848
|
+
GET /api/agents/{agent_id}
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
### Examples
|
|
852
|
+
|
|
853
|
+
**Python:**
|
|
854
|
+
```python
|
|
855
|
+
import requests, json
|
|
856
|
+
|
|
857
|
+
port = open('~/.sanqian/runtime/api.port').read().strip()
|
|
858
|
+
|
|
859
|
+
# Non-streaming
|
|
860
|
+
r = requests.post(f'http://localhost:{port}/api/agents/default/chat', json={
|
|
861
|
+
'messages': [{'role': 'user', 'content': 'Hello'}],
|
|
862
|
+
'stream': False,
|
|
863
|
+
})
|
|
864
|
+
print(r.json()['message']['content'])
|
|
865
|
+
|
|
866
|
+
# Streaming
|
|
867
|
+
r = requests.post(f'http://localhost:{port}/api/agents/default/chat', json={
|
|
868
|
+
'messages': [{'role': 'user', 'content': 'Hello'}],
|
|
869
|
+
'stream': True,
|
|
870
|
+
}, stream=True)
|
|
871
|
+
for line in r.iter_lines():
|
|
872
|
+
if line.startswith(b'data: '):
|
|
873
|
+
event = json.loads(line[6:])
|
|
874
|
+
if event['type'] == 'text':
|
|
875
|
+
print(event['content'], end='', flush=True)
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
**JavaScript (no SDK):**
|
|
879
|
+
```javascript
|
|
880
|
+
const port = require('fs').readFileSync(
|
|
881
|
+
require('os').homedir() + '/.sanqian/runtime/api.port', 'utf8'
|
|
882
|
+
).trim()
|
|
883
|
+
|
|
884
|
+
const response = await fetch(`http://localhost:${port}/api/agents/default/chat`, {
|
|
885
|
+
method: 'POST',
|
|
886
|
+
headers: { 'Content-Type': 'application/json' },
|
|
887
|
+
body: JSON.stringify({
|
|
888
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
889
|
+
stream: false,
|
|
890
|
+
}),
|
|
891
|
+
})
|
|
892
|
+
const data = await response.json()
|
|
893
|
+
console.log(data.message.content)
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
---
|
|
897
|
+
|
|
898
|
+
## Built-in Tools Reference
|
|
899
|
+
|
|
900
|
+
These tools are available to all agents. Use tool names in agent config to enable specific ones, or `["*"]` for all.
|
|
901
|
+
|
|
902
|
+
### File Operations
|
|
903
|
+
|
|
904
|
+
| Tool | Description |
|
|
905
|
+
|------|-------------|
|
|
906
|
+
| `read_file` | Read files from workspace |
|
|
907
|
+
| `write_file` | Write files to workspace |
|
|
908
|
+
| `edit_file` | Edit files with precise string replacement |
|
|
909
|
+
| `delete_file` | Delete files from workspace |
|
|
910
|
+
| `list_files` | List files in workspace directory |
|
|
911
|
+
| `find_files` | Find files by name pattern (glob) |
|
|
912
|
+
| `search_file` | Search for content within files |
|
|
913
|
+
| `grep_content` | Cross-file content search with regex |
|
|
914
|
+
|
|
915
|
+
### Web & Search
|
|
916
|
+
|
|
917
|
+
| Tool | Description |
|
|
918
|
+
|------|-------------|
|
|
919
|
+
| `web_search` | Search the web (Google Custom Search) |
|
|
920
|
+
| `fetch_web` | Fetch web pages and convert to markdown |
|
|
921
|
+
|
|
922
|
+
### Execution
|
|
923
|
+
|
|
924
|
+
| Tool | Description |
|
|
925
|
+
|------|-------------|
|
|
926
|
+
| `run_bash_command` | Execute shell commands in workspace (sandboxed) |
|
|
927
|
+
|
|
928
|
+
### Memory
|
|
929
|
+
|
|
930
|
+
| Tool | Description |
|
|
931
|
+
|------|-------------|
|
|
932
|
+
| `search_memory` | Search user memories by semantic similarity |
|
|
933
|
+
| `save_memory` | Save a new memory for the user |
|
|
934
|
+
| `list_memories` | List all user memories with optional filtering |
|
|
935
|
+
|
|
936
|
+
### Task & Agent
|
|
937
|
+
|
|
938
|
+
| Tool | Description |
|
|
939
|
+
|------|-------------|
|
|
940
|
+
| `todo_write` | Create and update task list |
|
|
941
|
+
| `task` | Delegate a task to another agent |
|
|
942
|
+
| `search_capability` | Search available tools, skills, and agents |
|
|
943
|
+
| `ask_human` | Ask user for input when additional information is needed |
|
|
944
|
+
|
|
945
|
+
### Vision & Image
|
|
946
|
+
|
|
947
|
+
| Tool | Description |
|
|
948
|
+
|------|-------------|
|
|
949
|
+
| `vision_analyze` | Analyze images using vision model |
|
|
950
|
+
| `generate_image` | Generate images from text |
|
|
951
|
+
|
|
952
|
+
### macOS Apple Integration (macOS only)
|
|
953
|
+
|
|
954
|
+
| Tool | Description |
|
|
955
|
+
|------|-------------|
|
|
956
|
+
| `calendar_search` / `calendar_create` / `calendar_delete` | Calendar operations |
|
|
957
|
+
| `notes_search` / `notes_create` / `notes_delete` | Notes operations |
|
|
958
|
+
| `reminders_search` / `reminders_create` / `reminders_complete` / `reminders_delete` | Reminders operations |
|
|
959
|
+
| `contacts_search` | Search contacts |
|
|
960
|
+
|
|
961
|
+
---
|
|
962
|
+
|
|
963
|
+
## FAQ
|
|
964
|
+
|
|
965
|
+
**How do I get the agent_id for chat?**
|
|
966
|
+
Use the List Agents API (`GET /api/agents`) or `sdk.listAgents()`. Common built-in IDs: `default`, `coding`.
|
|
967
|
+
|
|
968
|
+
**When to use HTTP API vs SDK?**
|
|
969
|
+
HTTP API for simple chat from any language. SDK when you need tools, agents, context providers, or session resources.
|
|
970
|
+
|
|
971
|
+
**Are SDK conversations visible in Sanqian?**
|
|
972
|
+
Yes. Conversations created via SDK appear in Sanqian's conversation list.
|
|
973
|
+
|
|
974
|
+
**What about tool naming conflicts?**
|
|
975
|
+
SDK tools are namespaced: `appName:toolName`. No conflicts between apps.
|
|
976
|
+
|
|
977
|
+
**Does the agent auto-reconnect after Sanqian restarts?**
|
|
978
|
+
Yes. The Node.js SDK watches `connection.json`. When Sanqian restarts and writes a new file, SDK reconnects automatically.
|
|
979
|
+
|
|
980
|
+
**What happens if Sanqian is not running when my app starts?**
|
|
981
|
+
With `autoLaunchSanqian: true` (default), SDK launches Sanqian in tray mode. Otherwise, SDK watches for `connection.json` and connects when Sanqian starts later.
|
|
982
|
+
|
|
983
|
+
## Error Codes
|
|
984
|
+
|
|
985
|
+
| Code | Description |
|
|
986
|
+
|------|-------------|
|
|
987
|
+
| `400` | Invalid request parameters |
|
|
988
|
+
| `404` | Agent or conversation not found |
|
|
989
|
+
| `429` | Rate limited |
|
|
990
|
+
| `500` | Internal server error |
|
|
991
|
+
|
|
992
|
+
---
|
|
993
|
+
|
|
994
|
+
<a name="中文"></a>
|
|
995
|
+
|
|
996
|
+
# 中文
|
|
997
|
+
|
|
998
|
+
Sanqian 提供两种外部集成方式:
|
|
999
|
+
|
|
1000
|
+
| 方式 | 适用场景 | 协议 |
|
|
1001
|
+
|------|----------|------|
|
|
1002
|
+
| **HTTP API** | 简单对话,任意语言 | REST + SSE |
|
|
1003
|
+
| **SDK** | 工具注册、Agent、上下文注入、深度集成 | WebSocket |
|
|
1004
|
+
|
|
1005
|
+
## 快速开始
|
|
1006
|
+
|
|
1007
|
+
### HTTP API
|
|
1008
|
+
|
|
1009
|
+
```bash
|
|
1010
|
+
# 获取端口(Sanqian 启动时写入)
|
|
1011
|
+
PORT=$(cat ~/.sanqian/runtime/api.port)
|
|
1012
|
+
|
|
1013
|
+
# 与默认 Agent 对话
|
|
1014
|
+
curl -X POST "http://localhost:$PORT/api/agents/default/chat" \
|
|
1015
|
+
-H "Content-Type: application/json" \
|
|
1016
|
+
-d '{"messages": [{"role": "user", "content": "你好"}], "stream": false}'
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
### SDK
|
|
1020
|
+
|
|
1021
|
+
```bash
|
|
1022
|
+
npm install @yushaw/sanqian-sdk
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
```typescript
|
|
1026
|
+
import { SanqianSDK } from '@yushaw/sanqian-sdk'
|
|
1027
|
+
|
|
1028
|
+
const sdk = new SanqianSDK({
|
|
1029
|
+
appName: 'my-app',
|
|
1030
|
+
appVersion: '1.0.0',
|
|
1031
|
+
tools: [{
|
|
1032
|
+
name: 'greet',
|
|
1033
|
+
description: '向用户打招呼',
|
|
1034
|
+
parameters: {
|
|
1035
|
+
type: 'object',
|
|
1036
|
+
properties: { name: { type: 'string' } },
|
|
1037
|
+
required: ['name']
|
|
1038
|
+
},
|
|
1039
|
+
handler: async ({ name }) => `你好,${name}!`
|
|
1040
|
+
}]
|
|
1041
|
+
})
|
|
1042
|
+
|
|
1043
|
+
// SDK 会在 Sanqian 启动时自动连接(通过监听 connection.json 文件变化)。
|
|
1044
|
+
// 如果 Sanqian 已在运行,则立即连接。
|
|
1045
|
+
// 如果 autoLaunchSanqian 为 true(默认),在 Sanqian 未运行时会自动启动它。
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
---
|
|
1049
|
+
|
|
1050
|
+
## 工具 (Tools)
|
|
1051
|
+
|
|
1052
|
+
工具是你的应用向 Sanqian 提供能力的主要方式。当用户提问时,AI Agent 可以调用你的工具来获取数据或执行操作。
|
|
1053
|
+
|
|
1054
|
+
### 定义工具
|
|
1055
|
+
|
|
1056
|
+
每个工具需要名称、描述(给 LLM 看的)、JSON Schema 参数和处理函数。
|
|
1057
|
+
|
|
1058
|
+
```typescript
|
|
1059
|
+
const sdk = new SanqianSDK({
|
|
1060
|
+
appName: 'my-notes-app',
|
|
1061
|
+
appVersion: '1.0.0',
|
|
1062
|
+
tools: [
|
|
1063
|
+
{
|
|
1064
|
+
name: 'search_notes',
|
|
1065
|
+
description: '按关键词搜索用户笔记',
|
|
1066
|
+
parameters: {
|
|
1067
|
+
type: 'object',
|
|
1068
|
+
properties: {
|
|
1069
|
+
query: { type: 'string', description: '搜索关键词' },
|
|
1070
|
+
limit: { type: 'number', description: '最大结果数', default: 10 }
|
|
1071
|
+
},
|
|
1072
|
+
required: ['query']
|
|
1073
|
+
},
|
|
1074
|
+
handler: async ({ query, limit }) => {
|
|
1075
|
+
const results = await db.searchNotes(query, limit)
|
|
1076
|
+
return results.map(n => ({ title: n.title, snippet: n.snippet }))
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
]
|
|
1080
|
+
})
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
工具名称会自动加上应用前缀。`search_notes` 在 Sanqian 中变为 `my-notes-app:search_notes`。
|
|
1084
|
+
|
|
1085
|
+
### 运行时更新工具
|
|
1086
|
+
|
|
1087
|
+
```typescript
|
|
1088
|
+
await sdk.updateTools([
|
|
1089
|
+
{ name: 'search_notes', /* 更新后的定义 */ handler: newHandler },
|
|
1090
|
+
{ name: 'new_tool', /* ... */ handler: newToolHandler }
|
|
1091
|
+
])
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
这会替换整个工具列表。新的 Agent 运行会立即使用更新后的工具。
|
|
1095
|
+
|
|
1096
|
+
### 工具可搜索性
|
|
1097
|
+
|
|
1098
|
+
默认情况下,工具可以通过 Sanqian 的 `search_capability` 工具被发现。设置 `searchable: false` 隐藏工具但保持可用:
|
|
1099
|
+
|
|
1100
|
+
```typescript
|
|
1101
|
+
{
|
|
1102
|
+
name: 'internal_sync',
|
|
1103
|
+
description: '内部数据同步',
|
|
1104
|
+
searchable: false, // 可用但不可被搜索发现
|
|
1105
|
+
parameters: { type: 'object' },
|
|
1106
|
+
handler: async () => { /* ... */ }
|
|
1107
|
+
}
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
---
|
|
1111
|
+
|
|
1112
|
+
## Agent
|
|
1113
|
+
|
|
1114
|
+
私有 Agent 是具有特定工具、技能和指令的自定义 AI 角色。它们会出现在 Sanqian 的 Agent 选择器中。
|
|
1115
|
+
|
|
1116
|
+
### 创建 Agent
|
|
1117
|
+
|
|
1118
|
+
```typescript
|
|
1119
|
+
const agent = await sdk.createAgent({
|
|
1120
|
+
agent_id: 'notes-assistant',
|
|
1121
|
+
name: '笔记助手',
|
|
1122
|
+
description: '帮助管理和搜索笔记',
|
|
1123
|
+
system_prompt: '你是一个笔记助手。帮助用户整理笔记。',
|
|
1124
|
+
tools: ['search_notes', 'create_note'], // SDK 工具(自动加前缀)
|
|
1125
|
+
skills: ['web-research'], // Sanqian 内置技能
|
|
1126
|
+
subagents: ['*'], // 可以委派给任何 Agent
|
|
1127
|
+
searchable: true, // 可被其他 Agent 发现
|
|
1128
|
+
})
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
### 工具名称格式
|
|
1132
|
+
|
|
1133
|
+
| 格式 | 示例 | 说明 |
|
|
1134
|
+
|------|------|------|
|
|
1135
|
+
| 短名称 | `"search_notes"` | 你的 SDK 工具(自动加应用前缀) |
|
|
1136
|
+
| 完整 SDK 名称 | `"other-app:tool_name"` | 其他 SDK 应用的工具 |
|
|
1137
|
+
| 内置 | `"read_file"`, `"run_bash_command"` | Sanqian 内置工具 |
|
|
1138
|
+
| MCP | `"mcp_servername_toolname"` | MCP 服务器工具 |
|
|
1139
|
+
| 通配符 | `["*"]` | 所有可用工具 |
|
|
1140
|
+
|
|
1141
|
+
### 子 Agent
|
|
1142
|
+
|
|
1143
|
+
控制你的 Agent 是否可以委派任务给其他 Agent:
|
|
1144
|
+
|
|
1145
|
+
```typescript
|
|
1146
|
+
{
|
|
1147
|
+
subagents: undefined, // 不能使用 task 工具(默认)
|
|
1148
|
+
subagents: [], // 不能使用 task 工具(显式)
|
|
1149
|
+
subagents: ['*'], // 可以调用任何 Agent
|
|
1150
|
+
subagents: ['agent1', 'agent2'] // 只能调用指定的 Agent
|
|
1151
|
+
}
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
### 更新和删除
|
|
1155
|
+
|
|
1156
|
+
```typescript
|
|
1157
|
+
// 更新特定字段(其他不变)
|
|
1158
|
+
await sdk.updateAgent('notes-assistant', {
|
|
1159
|
+
system_prompt: '更新后的指令...',
|
|
1160
|
+
tools: ['search_notes', 'create_note', 'delete_note']
|
|
1161
|
+
})
|
|
1162
|
+
|
|
1163
|
+
// 删除
|
|
1164
|
+
await sdk.deleteAgent('notes-assistant')
|
|
1165
|
+
|
|
1166
|
+
// 列出你的 Agent
|
|
1167
|
+
const agents = await sdk.listAgents()
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
---
|
|
1171
|
+
|
|
1172
|
+
## 对话 (Chat)
|
|
1173
|
+
|
|
1174
|
+
向任何 Agent 发送消息并获取回复。支持流式和非流式模式。
|
|
1175
|
+
|
|
1176
|
+
### 非流式
|
|
1177
|
+
|
|
1178
|
+
```typescript
|
|
1179
|
+
const response = await sdk.chat('notes-assistant', [
|
|
1180
|
+
{ role: 'user', content: '找到我关于 TypeScript 的笔记' }
|
|
1181
|
+
])
|
|
1182
|
+
|
|
1183
|
+
console.log(response.message.content)
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
### 流式
|
|
1187
|
+
|
|
1188
|
+
```typescript
|
|
1189
|
+
for await (const event of sdk.chatStream('notes-assistant', [
|
|
1190
|
+
{ role: 'user', content: '总结我最近的笔记' }
|
|
1191
|
+
])) {
|
|
1192
|
+
switch (event.type) {
|
|
1193
|
+
case 'start':
|
|
1194
|
+
// 流开始,event.run_id 可用
|
|
1195
|
+
break
|
|
1196
|
+
case 'text':
|
|
1197
|
+
process.stdout.write(event.content || '')
|
|
1198
|
+
break
|
|
1199
|
+
case 'thinking':
|
|
1200
|
+
// 推理内容(DeepSeek R1、o3 等)
|
|
1201
|
+
break
|
|
1202
|
+
case 'tool_call':
|
|
1203
|
+
console.log(`调用工具: ${event.tool_call?.function.name}`)
|
|
1204
|
+
break
|
|
1205
|
+
case 'tool_args_chunk':
|
|
1206
|
+
// 流式工具参数(部分 JSON)
|
|
1207
|
+
break
|
|
1208
|
+
case 'tool_result':
|
|
1209
|
+
console.log(`结果: ${event.success ? '成功' : event.error}`)
|
|
1210
|
+
break
|
|
1211
|
+
case 'done':
|
|
1212
|
+
console.log(`\n会话: ${event.conversationId}`)
|
|
1213
|
+
break
|
|
1214
|
+
case 'error':
|
|
1215
|
+
console.error(event.error)
|
|
1216
|
+
break
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
```
|
|
1220
|
+
|
|
1221
|
+
### 流事件
|
|
1222
|
+
|
|
1223
|
+
| 事件 | 字段 | 说明 |
|
|
1224
|
+
|------|------|------|
|
|
1225
|
+
| `start` | `run_id`, `conversationId` | 流开始 |
|
|
1226
|
+
| `text` | `content` | AI 文本片段 |
|
|
1227
|
+
| `thinking` | `content` | 推理内容(思考模型) |
|
|
1228
|
+
| `tool_call` | `tool_call` | 工具调用开始 |
|
|
1229
|
+
| `tool_args_chunk` | `tool_call_id`, `tool_name`, `chunk` | 流式工具参数 |
|
|
1230
|
+
| `tool_args` | `tool_call_id`, `tool_name`, `args` | 完整工具参数 |
|
|
1231
|
+
| `tool_result` | `tool_call_id`, `result`, `success`, `error` | 工具执行结果 |
|
|
1232
|
+
| `interrupt` | `interrupt_type`, `interrupt_payload`, `run_id` | HITL 暂停 |
|
|
1233
|
+
| `done` | `conversationId`, `title` | 流结束 |
|
|
1234
|
+
| `error` | `error` | 发生错误 |
|
|
1235
|
+
| `cancelled` | `run_id` | 运行被取消 |
|
|
1236
|
+
|
|
1237
|
+
### 有状态 vs 无状态
|
|
1238
|
+
|
|
1239
|
+
```typescript
|
|
1240
|
+
// 无状态:你自己管理消息历史
|
|
1241
|
+
const r1 = await sdk.chat('agent', messages)
|
|
1242
|
+
// r1.conversationId 是空字符串
|
|
1243
|
+
|
|
1244
|
+
// 有状态:服务器管理历史
|
|
1245
|
+
const r1 = await sdk.chat('agent', messages, {
|
|
1246
|
+
persistHistory: true // 创建服务端会话
|
|
1247
|
+
})
|
|
1248
|
+
// r1.conversationId 有值,后续使用它
|
|
1249
|
+
|
|
1250
|
+
const r2 = await sdk.chat('agent', [{ role: 'user', content: '继续' }], {
|
|
1251
|
+
conversationId: r1.conversationId
|
|
1252
|
+
})
|
|
1253
|
+
```
|
|
1254
|
+
|
|
1255
|
+
### 会话助手
|
|
1256
|
+
|
|
1257
|
+
多轮对话可以使用 `Conversation` 助手:
|
|
1258
|
+
|
|
1259
|
+
```typescript
|
|
1260
|
+
const conv = sdk.startConversation('notes-assistant')
|
|
1261
|
+
|
|
1262
|
+
const r1 = await conv.send('找到我的 TypeScript 笔记')
|
|
1263
|
+
const r2 = await conv.send('总结第一个')
|
|
1264
|
+
console.log(conv.id) // 会话 ID(首次 send 后可用)
|
|
1265
|
+
|
|
1266
|
+
// 流式
|
|
1267
|
+
for await (const event of conv.sendStream('还有更多细节吗?')) {
|
|
1268
|
+
if (event.type === 'text') process.stdout.write(event.content || '')
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// 获取历史
|
|
1272
|
+
const details = await conv.getDetails({ messageLimit: 50 })
|
|
1273
|
+
|
|
1274
|
+
// 删除
|
|
1275
|
+
await conv.delete()
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
### 取消运行
|
|
1279
|
+
|
|
1280
|
+
```typescript
|
|
1281
|
+
let runId: string | undefined
|
|
1282
|
+
|
|
1283
|
+
for await (const event of sdk.chatStream('agent', messages)) {
|
|
1284
|
+
if (event.type === 'start') runId = event.run_id
|
|
1285
|
+
if (shouldCancel) {
|
|
1286
|
+
sdk.cancelRun(runId!)
|
|
1287
|
+
break
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
### 自动发现
|
|
1293
|
+
|
|
1294
|
+
为对话启用自动发现技能、工具或子 Agent:
|
|
1295
|
+
|
|
1296
|
+
```typescript
|
|
1297
|
+
const response = await sdk.chat('agent', messages, {
|
|
1298
|
+
autoDiscoverSkills: true, // Agent 可以搜索并使用技能
|
|
1299
|
+
autoDiscoverTools: true, // Agent 可以搜索并使用工具
|
|
1300
|
+
autoDiscoverSubagents: true // Agent 可以委派给其他 Agent
|
|
1301
|
+
})
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
---
|
|
1305
|
+
|
|
1306
|
+
## 人机协作 (HITL)
|
|
1307
|
+
|
|
1308
|
+
HITL 允许 Agent 在运行中暂停并请求用户输入。三种中断类型:
|
|
1309
|
+
|
|
1310
|
+
| 类型 | 用途 | 示例 |
|
|
1311
|
+
|------|------|------|
|
|
1312
|
+
| `approval_request` | 执行前审批 | "删除文件 X?" -> 批准/拒绝 |
|
|
1313
|
+
| `user_input_request` | 请求用户输入 | "什么格式?" -> ["JSON", "CSV"] 或自由文本 |
|
|
1314
|
+
| `user_action_request` | 用户完成外部操作 | "需要登录" -> 用户登录 -> "完成" |
|
|
1315
|
+
|
|
1316
|
+
### 处理中断
|
|
1317
|
+
|
|
1318
|
+
```typescript
|
|
1319
|
+
for await (const event of sdk.chatStream('agent', messages)) {
|
|
1320
|
+
if (event.type === 'interrupt') {
|
|
1321
|
+
const { interrupt_type, interrupt_payload, run_id } = event
|
|
1322
|
+
|
|
1323
|
+
if (interrupt_type === 'approval_request') {
|
|
1324
|
+
const approved = await showApprovalDialog(interrupt_payload)
|
|
1325
|
+
sdk.sendHitlResponse(run_id!, { approved })
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
if (interrupt_type === 'user_input_request') {
|
|
1329
|
+
const answer = await showInputDialog(interrupt_payload)
|
|
1330
|
+
sdk.sendHitlResponse(run_id!, { answer })
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
---
|
|
1337
|
+
|
|
1338
|
+
## 会话管理 (Conversations)
|
|
1339
|
+
|
|
1340
|
+
管理服务器上存储的对话历史。
|
|
1341
|
+
|
|
1342
|
+
```typescript
|
|
1343
|
+
// 列出会话
|
|
1344
|
+
const { conversations, total } = await sdk.listConversations({
|
|
1345
|
+
agentId: 'notes-assistant',
|
|
1346
|
+
limit: 20,
|
|
1347
|
+
offset: 0
|
|
1348
|
+
})
|
|
1349
|
+
|
|
1350
|
+
// 获取会话详情(含消息)
|
|
1351
|
+
const detail = await sdk.getConversation(conversationId, {
|
|
1352
|
+
includeMessages: true,
|
|
1353
|
+
messageLimit: 50,
|
|
1354
|
+
})
|
|
1355
|
+
|
|
1356
|
+
// 获取消息历史(通过 HTTP API,与主应用一致)
|
|
1357
|
+
const history = await sdk.getMessages(conversationId, { limit: 50 })
|
|
1358
|
+
|
|
1359
|
+
// 删除会话
|
|
1360
|
+
await sdk.deleteConversation(conversationId)
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
---
|
|
1364
|
+
|
|
1365
|
+
## 上下文提供者 (Context Providers)
|
|
1366
|
+
|
|
1367
|
+
上下文提供者让你的应用向对话注入动态状态。当用户附加上下文时,Sanqian 调用你的提供者获取最新数据。
|
|
1368
|
+
|
|
1369
|
+
三种提供者方法,均为可选:
|
|
1370
|
+
|
|
1371
|
+
| 方法 | 用途 | 调用时机 |
|
|
1372
|
+
|------|------|----------|
|
|
1373
|
+
| `getCurrent()` | 获取当前状态 | 用户发送带上下文的消息 |
|
|
1374
|
+
| `getList(options)` | 列出可用资源 | 用户打开 "+" 菜单浏览 |
|
|
1375
|
+
| `getById(id)` | 获取特定资源 | 用户从列表中选择 |
|
|
1376
|
+
|
|
1377
|
+
### 注册上下文提供者
|
|
1378
|
+
|
|
1379
|
+
```typescript
|
|
1380
|
+
const sdk = new SanqianSDK({
|
|
1381
|
+
appName: 'my-notes-app',
|
|
1382
|
+
appVersion: '1.0.0',
|
|
1383
|
+
tools: [/* ... */],
|
|
1384
|
+
contexts: [
|
|
1385
|
+
{
|
|
1386
|
+
id: 'active-note',
|
|
1387
|
+
name: '当前笔记',
|
|
1388
|
+
description: '正在编辑的笔记',
|
|
1389
|
+
getCurrent: async () => ({
|
|
1390
|
+
content: editor.getCurrentNote().content,
|
|
1391
|
+
title: editor.getCurrentNote().title,
|
|
1392
|
+
type: 'note',
|
|
1393
|
+
}),
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
id: 'notes',
|
|
1397
|
+
name: '笔记库',
|
|
1398
|
+
description: '浏览并附加笔记',
|
|
1399
|
+
getList: async (options) => {
|
|
1400
|
+
const notes = await db.searchNotes(options?.query || '', {
|
|
1401
|
+
offset: options?.offset || 0,
|
|
1402
|
+
limit: options?.limit || 20,
|
|
1403
|
+
})
|
|
1404
|
+
return {
|
|
1405
|
+
items: notes.map(n => ({
|
|
1406
|
+
id: n.id,
|
|
1407
|
+
title: n.title,
|
|
1408
|
+
summary: n.snippet,
|
|
1409
|
+
type: 'note',
|
|
1410
|
+
group: n.folder,
|
|
1411
|
+
})),
|
|
1412
|
+
hasMore: notes.length === (options?.limit || 20),
|
|
1413
|
+
}
|
|
1414
|
+
},
|
|
1415
|
+
getById: async (id) => {
|
|
1416
|
+
const note = await db.getNote(id)
|
|
1417
|
+
if (!note) return null
|
|
1418
|
+
return { id: note.id, content: note.content, title: note.title, type: 'note' }
|
|
1419
|
+
},
|
|
1420
|
+
}
|
|
1421
|
+
]
|
|
1422
|
+
})
|
|
1423
|
+
```
|
|
1424
|
+
|
|
1425
|
+
### 将上下文附加到 Agent
|
|
1426
|
+
|
|
1427
|
+
```typescript
|
|
1428
|
+
await sdk.createAgent({
|
|
1429
|
+
agent_id: 'notes-assistant',
|
|
1430
|
+
name: '笔记助手',
|
|
1431
|
+
tools: ['search_notes'],
|
|
1432
|
+
attached_contexts: ['active-note'], // 自动加应用前缀
|
|
1433
|
+
})
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
### 运行时更新上下文
|
|
1437
|
+
|
|
1438
|
+
```typescript
|
|
1439
|
+
await sdk.updateContexts([
|
|
1440
|
+
{ id: 'active-note', name: '当前笔记', description: '...', getCurrent: newHandler },
|
|
1441
|
+
])
|
|
1442
|
+
```
|
|
1443
|
+
|
|
1444
|
+
---
|
|
1445
|
+
|
|
1446
|
+
## 会话资源 (Session Resources)
|
|
1447
|
+
|
|
1448
|
+
会话资源是你的应用推送给 Sanqian 的临时上下文。与上下文提供者(拉取式)不同,会话资源是推送式的。
|
|
1449
|
+
|
|
1450
|
+
```typescript
|
|
1451
|
+
// 推送资源
|
|
1452
|
+
const stored = await sdk.pushResource({
|
|
1453
|
+
title: '当前笔记',
|
|
1454
|
+
content: '<note>\n# 我的笔记\n内容...\n</note>',
|
|
1455
|
+
summary: '我的笔记 - 2024-01-15',
|
|
1456
|
+
})
|
|
1457
|
+
console.log(stored.fullId) // "my-notes-app:abc123"
|
|
1458
|
+
|
|
1459
|
+
// 移除资源
|
|
1460
|
+
await sdk.removeResource('my-notes-app:abc123')
|
|
1461
|
+
|
|
1462
|
+
// 清除所有资源
|
|
1463
|
+
await sdk.clearResources()
|
|
1464
|
+
|
|
1465
|
+
// 本地缓存(无网络请求)
|
|
1466
|
+
const resources = sdk.getSessionResources()
|
|
1467
|
+
|
|
1468
|
+
// 从服务器获取
|
|
1469
|
+
const serverResources = await sdk.fetchSessionResources('notes-assistant')
|
|
1470
|
+
```
|
|
1471
|
+
|
|
1472
|
+
### 在对话中附加会话资源
|
|
1473
|
+
|
|
1474
|
+
```typescript
|
|
1475
|
+
for await (const event of sdk.chatStream('agent', messages, {
|
|
1476
|
+
sessionResources: ['my-notes-app:abc123'],
|
|
1477
|
+
})) { /* ... */ }
|
|
1478
|
+
```
|
|
1479
|
+
|
|
1480
|
+
---
|
|
1481
|
+
|
|
1482
|
+
## 能力发现 (Capability Discovery)
|
|
1483
|
+
|
|
1484
|
+
查询 Sanqian 的完整能力注册表。
|
|
1485
|
+
|
|
1486
|
+
```typescript
|
|
1487
|
+
// 列出工具
|
|
1488
|
+
const tools = await sdk.listTools()
|
|
1489
|
+
const builtinTools = await sdk.listTools('builtin')
|
|
1490
|
+
|
|
1491
|
+
// 列出技能
|
|
1492
|
+
const skills = await sdk.listSkills()
|
|
1493
|
+
|
|
1494
|
+
// 列出所有可用 Agent
|
|
1495
|
+
const agents = await sdk.listAvailableAgents()
|
|
1496
|
+
|
|
1497
|
+
// 语义搜索(BM25 + 向量混合搜索)
|
|
1498
|
+
const results = await sdk.searchCapabilities('文件操作', {
|
|
1499
|
+
type: 'tool',
|
|
1500
|
+
limit: 5,
|
|
1501
|
+
})
|
|
1502
|
+
```
|
|
1503
|
+
|
|
1504
|
+
---
|
|
1505
|
+
|
|
1506
|
+
## Embedding 与 Rerank 配置
|
|
1507
|
+
|
|
1508
|
+
复用 Sanqian 中配置的嵌入和重排序模型。
|
|
1509
|
+
|
|
1510
|
+
```typescript
|
|
1511
|
+
const embedding = await sdk.getEmbeddingConfig()
|
|
1512
|
+
if (embedding.available) {
|
|
1513
|
+
// 使用 embedding.apiUrl, embedding.apiKey, embedding.modelName
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const rerank = await sdk.getRerankConfig()
|
|
1517
|
+
if (rerank.available) {
|
|
1518
|
+
// 使用 rerank.apiUrl, rerank.apiKey, rerank.modelName
|
|
1519
|
+
}
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
---
|
|
1523
|
+
|
|
1524
|
+
## 连接
|
|
1525
|
+
|
|
1526
|
+
### 连接生命周期
|
|
1527
|
+
|
|
1528
|
+
SDK 自动管理连接:
|
|
1529
|
+
|
|
1530
|
+
1. **构造函数**:读取 `~/.sanqian/runtime/connection.json` 并监听变化
|
|
1531
|
+
2. **自动连接**:connection.json 出现时(Sanqian 启动),SDK 自动连接
|
|
1532
|
+
3. **自动启动**:`autoLaunchSanqian: true`(默认),未运行时自动启动 Sanqian
|
|
1533
|
+
4. **注册**:WebSocket 连接后,注册应用名称、工具和上下文提供者
|
|
1534
|
+
5. **心跳**:30 秒间隔检测连接状态
|
|
1535
|
+
6. **自动重连**:指数退避(500ms 到 5s)带抖动
|
|
1536
|
+
|
|
1537
|
+
### 重连控制
|
|
1538
|
+
|
|
1539
|
+
自动重连是引用计数的:
|
|
1540
|
+
|
|
1541
|
+
```typescript
|
|
1542
|
+
sdk.acquireReconnect() // 聊天面板打开 - 请求持久连接
|
|
1543
|
+
sdk.releaseReconnect() // 聊天面板关闭 - 释放
|
|
1544
|
+
```
|
|
1545
|
+
|
|
1546
|
+
### 连接状态
|
|
1547
|
+
|
|
1548
|
+
```typescript
|
|
1549
|
+
const state = sdk.getState()
|
|
1550
|
+
// { connected, registering, registered, lastError?, reconnectAttempts }
|
|
1551
|
+
|
|
1552
|
+
sdk.isConnected() // true 当已连接且已注册
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
### 事件
|
|
1556
|
+
|
|
1557
|
+
```typescript
|
|
1558
|
+
sdk.on('connected', () => { /* WebSocket 已打开 */ })
|
|
1559
|
+
sdk.on('disconnected', (reason) => { /* 连接断开 */ })
|
|
1560
|
+
sdk.on('registered', () => { /* 工具和上下文已注册 */ })
|
|
1561
|
+
sdk.on('error', (error) => { /* 连接错误 */ })
|
|
1562
|
+
sdk.on('tool_call', ({ name, arguments }) => { /* 工具被调用 */ })
|
|
1563
|
+
sdk.on('resourcePushed', (resource) => { /* 会话资源已推送 */ })
|
|
1564
|
+
sdk.on('resourceRemoved', (resourceId) => { /* 用户移除了会话资源 */ })
|
|
1565
|
+
sdk.on('resourcesCleared', (appName) => { /* 所有资源已清除 */ })
|
|
1566
|
+
|
|
1567
|
+
sdk.once('registered', () => { /* 一次性监听 */ })
|
|
1568
|
+
sdk.removeAllListeners() // 移除所有监听器
|
|
1569
|
+
```
|
|
1570
|
+
|
|
1571
|
+
### SDKConfig 选项
|
|
1572
|
+
|
|
1573
|
+
```typescript
|
|
1574
|
+
const sdk = new SanqianSDK({
|
|
1575
|
+
// 必填
|
|
1576
|
+
appName: 'my-app',
|
|
1577
|
+
appVersion: '1.0.0',
|
|
1578
|
+
tools: [],
|
|
1579
|
+
|
|
1580
|
+
// 显示
|
|
1581
|
+
displayName: 'My App', // Sanqian UI 中显示的名称
|
|
1582
|
+
|
|
1583
|
+
// 启动
|
|
1584
|
+
launchCommand: '/path/to/app', // Sanqian 启动你的应用的命令
|
|
1585
|
+
metadata: { browser: 'chrome' }, // 应用元数据
|
|
1586
|
+
|
|
1587
|
+
// 上下文
|
|
1588
|
+
contexts: [], // 上下文提供者
|
|
1589
|
+
|
|
1590
|
+
// 超时
|
|
1591
|
+
reconnectInterval: 5000, // 重连间隔(毫秒,默认 5000)
|
|
1592
|
+
heartbeatInterval: 30000, // 心跳间隔(毫秒,默认 30000)
|
|
1593
|
+
toolExecutionTimeout: 30000, // 工具超时(毫秒,默认 30000)
|
|
1594
|
+
|
|
1595
|
+
// 自动启动
|
|
1596
|
+
autoLaunchSanqian: true, // 未运行时启动 Sanqian(默认 true)
|
|
1597
|
+
sanqianPath: '/path/to/Sanqian', // 自定义可执行文件路径
|
|
1598
|
+
|
|
1599
|
+
// 调试
|
|
1600
|
+
debug: false, // 控制台日志(默认 false)
|
|
1601
|
+
|
|
1602
|
+
// 浏览器模式
|
|
1603
|
+
connectionInfo: undefined, // 预配置连接信息(跳过文件发现)
|
|
1604
|
+
})
|
|
1605
|
+
```
|
|
1606
|
+
|
|
1607
|
+
---
|
|
1608
|
+
|
|
1609
|
+
## 浏览器构建 (Browser Build)
|
|
1610
|
+
|
|
1611
|
+
用于浏览器环境(扩展、Web 应用、Office 插件):
|
|
1612
|
+
|
|
1613
|
+
```typescript
|
|
1614
|
+
import { SanqianSDK } from '@yushaw/sanqian-sdk/browser'
|
|
1615
|
+
|
|
1616
|
+
const sdk = new SanqianSDK({
|
|
1617
|
+
appName: 'my-extension',
|
|
1618
|
+
appVersion: '1.0.0',
|
|
1619
|
+
connectionInfo: {
|
|
1620
|
+
port: 38765,
|
|
1621
|
+
token: 'your-token',
|
|
1622
|
+
ws_path: '/ws/apps',
|
|
1623
|
+
version: 1,
|
|
1624
|
+
pid: 0,
|
|
1625
|
+
started_at: '',
|
|
1626
|
+
},
|
|
1627
|
+
tools: [/* ... */]
|
|
1628
|
+
})
|
|
1629
|
+
|
|
1630
|
+
await sdk.connect()
|
|
1631
|
+
```
|
|
1632
|
+
|
|
1633
|
+
浏览器构建使用原生 WebSocket,需要提供 `connectionInfo`,不支持 `autoLaunchSanqian` 和 connection.json 文件监听。其他所有功能均可使用。
|
|
1634
|
+
|
|
1635
|
+
---
|
|
1636
|
+
|
|
1637
|
+
## HTTP API 参考
|
|
1638
|
+
|
|
1639
|
+
简单集成无需 SDK。基础 URL:`http://localhost:{PORT}`(端口来自 `~/.sanqian/runtime/api.port`)。
|
|
1640
|
+
|
|
1641
|
+
### 对话
|
|
1642
|
+
|
|
1643
|
+
```
|
|
1644
|
+
POST /api/agents/{agent_id}/chat
|
|
1645
|
+
```
|
|
1646
|
+
|
|
1647
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
1648
|
+
|------|------|------|------|
|
|
1649
|
+
| `messages` | `ChatMessage[]` | 是 | 发送的消息 |
|
|
1650
|
+
| `stream` | `boolean` | 否 | 启用 SSE 流(默认 true) |
|
|
1651
|
+
| `conversation_id` | `string` | 否 | 继续已有会话 |
|
|
1652
|
+
|
|
1653
|
+
### 列出 Agent
|
|
1654
|
+
|
|
1655
|
+
```
|
|
1656
|
+
GET /api/agents
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
### 示例
|
|
1660
|
+
|
|
1661
|
+
**Python:**
|
|
1662
|
+
```python
|
|
1663
|
+
import requests, json
|
|
1664
|
+
|
|
1665
|
+
port = open('~/.sanqian/runtime/api.port').read().strip()
|
|
1666
|
+
|
|
1667
|
+
r = requests.post(f'http://localhost:{port}/api/agents/default/chat', json={
|
|
1668
|
+
'messages': [{'role': 'user', 'content': '你好'}],
|
|
1669
|
+
'stream': False,
|
|
1670
|
+
})
|
|
1671
|
+
print(r.json()['message']['content'])
|
|
1672
|
+
```
|
|
1673
|
+
|
|
1674
|
+
**JavaScript(无 SDK):**
|
|
1675
|
+
```javascript
|
|
1676
|
+
const port = require('fs').readFileSync(
|
|
1677
|
+
require('os').homedir() + '/.sanqian/runtime/api.port', 'utf8'
|
|
1678
|
+
).trim()
|
|
1679
|
+
|
|
1680
|
+
const response = await fetch(`http://localhost:${port}/api/agents/default/chat`, {
|
|
1681
|
+
method: 'POST',
|
|
1682
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1683
|
+
body: JSON.stringify({
|
|
1684
|
+
messages: [{ role: 'user', content: '你好' }],
|
|
1685
|
+
stream: false,
|
|
1686
|
+
}),
|
|
1687
|
+
})
|
|
1688
|
+
const data = await response.json()
|
|
1689
|
+
console.log(data.message.content)
|
|
1690
|
+
```
|
|
1691
|
+
|
|
1692
|
+
---
|
|
1693
|
+
|
|
1694
|
+
## 内置工具参考
|
|
1695
|
+
|
|
1696
|
+
### 文件操作
|
|
1697
|
+
|
|
1698
|
+
| 工具 | 说明 |
|
|
1699
|
+
|------|------|
|
|
1700
|
+
| `read_file` | 读取工作区文件 |
|
|
1701
|
+
| `write_file` | 写入工作区文件 |
|
|
1702
|
+
| `edit_file` | 精确字符串替换编辑文件 |
|
|
1703
|
+
| `delete_file` | 删除工作区文件 |
|
|
1704
|
+
| `list_files` | 列出目录内容 |
|
|
1705
|
+
| `find_files` | 按 glob 模式查找文件 |
|
|
1706
|
+
| `search_file` | 搜索文件内容 |
|
|
1707
|
+
| `grep_content` | 跨文件正则内容搜索 |
|
|
1708
|
+
|
|
1709
|
+
### 网络
|
|
1710
|
+
|
|
1711
|
+
| 工具 | 说明 |
|
|
1712
|
+
|------|------|
|
|
1713
|
+
| `web_search` | 网络搜索 |
|
|
1714
|
+
| `fetch_web` | 获取网页并转换为 Markdown |
|
|
1715
|
+
|
|
1716
|
+
### 执行
|
|
1717
|
+
|
|
1718
|
+
| 工具 | 说明 |
|
|
1719
|
+
|------|------|
|
|
1720
|
+
| `run_bash_command` | 执行 Shell 命令(沙箱化) |
|
|
1721
|
+
|
|
1722
|
+
### 记忆
|
|
1723
|
+
|
|
1724
|
+
| 工具 | 说明 |
|
|
1725
|
+
|------|------|
|
|
1726
|
+
| `search_memory` | 按语义相似度搜索记忆 |
|
|
1727
|
+
| `save_memory` | 保存新记忆 |
|
|
1728
|
+
| `list_memories` | 列出所有记忆 |
|
|
1729
|
+
|
|
1730
|
+
### 任务与 Agent
|
|
1731
|
+
|
|
1732
|
+
| 工具 | 说明 |
|
|
1733
|
+
|------|------|
|
|
1734
|
+
| `todo_write` | 创建和更新任务列表 |
|
|
1735
|
+
| `task` | 委派任务给其他 Agent |
|
|
1736
|
+
| `search_capability` | 搜索可用工具/技能/Agent |
|
|
1737
|
+
| `ask_human` | 需要更多信息时询问用户 |
|
|
1738
|
+
|
|
1739
|
+
### 视觉与图像
|
|
1740
|
+
|
|
1741
|
+
| 工具 | 说明 |
|
|
1742
|
+
|------|------|
|
|
1743
|
+
| `vision_analyze` | 图像分析 |
|
|
1744
|
+
| `generate_image` | 文本生成图像 |
|
|
1745
|
+
|
|
1746
|
+
### macOS Apple 集成(仅 macOS)
|
|
1747
|
+
|
|
1748
|
+
| 工具 | 说明 |
|
|
1749
|
+
|------|------|
|
|
1750
|
+
| `calendar_search` / `calendar_create` / `calendar_delete` | 日历操作 |
|
|
1751
|
+
| `notes_search` / `notes_create` / `notes_delete` | 备忘录操作 |
|
|
1752
|
+
| `reminders_search` / `reminders_create` / `reminders_complete` / `reminders_delete` | 提醒事项操作 |
|
|
1753
|
+
| `contacts_search` | 搜索通讯录 |
|
|
1754
|
+
|
|
1755
|
+
---
|
|
1756
|
+
|
|
1757
|
+
## 常见问题
|
|
1758
|
+
|
|
1759
|
+
**如何获取 agent_id?**
|
|
1760
|
+
使用 `GET /api/agents` 或 `sdk.listAgents()`。常见内置 ID:`default`、`coding`。
|
|
1761
|
+
|
|
1762
|
+
**什么时候用 HTTP API vs SDK?**
|
|
1763
|
+
HTTP API 适合任意语言的简单对话。SDK 适合需要工具、Agent、上下文提供者或会话资源的场景。
|
|
1764
|
+
|
|
1765
|
+
**SDK 会话在 Sanqian 中可见吗?**
|
|
1766
|
+
是的。通过 SDK 创建的会话会出现在 Sanqian 的会话列表中。
|
|
1767
|
+
|
|
1768
|
+
**Sanqian 重启后 Agent 会自动重连吗?**
|
|
1769
|
+
是的。Node.js SDK 监听 `connection.json`。Sanqian 重启写入新文件后,SDK 自动重连。
|
|
1770
|
+
|
|
1771
|
+
**如果启动时 Sanqian 未运行怎么办?**
|
|
1772
|
+
`autoLaunchSanqian: true`(默认)时,SDK 会在托盘模式下启动 Sanqian。否则 SDK 监听 `connection.json`,Sanqian 启动后自动连接。
|
|
1773
|
+
|
|
1774
|
+
## 错误码
|
|
1775
|
+
|
|
1776
|
+
| 码 | 说明 |
|
|
1777
|
+
|----|------|
|
|
1778
|
+
| `400` | 请求参数无效 |
|
|
1779
|
+
| `404` | Agent 或会话未找到 |
|
|
1780
|
+
| `429` | 频率限制 |
|
|
1781
|
+
| `500` | 内部服务器错误 |
|