@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 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` | 内部服务器错误 |