@yushaw/sanqian-sdk 0.1.0

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/dist/index.mjs ADDED
@@ -0,0 +1,1069 @@
1
+ // src/client.ts
2
+ import WebSocket from "ws";
3
+
4
+ // src/discovery.ts
5
+ import { existsSync, readFileSync, watch } from "fs";
6
+ import { homedir, platform } from "os";
7
+ import { join } from "path";
8
+ import { spawn } from "child_process";
9
+ var DiscoveryManager = class {
10
+ connectionInfo = null;
11
+ watcher = null;
12
+ onChange = null;
13
+ pollInterval = null;
14
+ /**
15
+ * Get the path to connection.json
16
+ */
17
+ getConnectionFilePath() {
18
+ return join(homedir(), ".sanqian", "runtime", "connection.json");
19
+ }
20
+ /**
21
+ * Read and validate connection info
22
+ *
23
+ * Returns null if:
24
+ * - File doesn't exist
25
+ * - File is invalid JSON
26
+ * - Process is not running
27
+ */
28
+ read() {
29
+ const filePath = this.getConnectionFilePath();
30
+ if (!existsSync(filePath)) {
31
+ return null;
32
+ }
33
+ try {
34
+ const content = readFileSync(filePath, "utf-8");
35
+ const info = JSON.parse(content);
36
+ if (!info.port || !info.token || !info.pid) {
37
+ return null;
38
+ }
39
+ if (!this.isProcessRunning(info.pid)) {
40
+ return null;
41
+ }
42
+ this.connectionInfo = info;
43
+ return info;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ /**
49
+ * Start watching for connection file changes
50
+ */
51
+ startWatching(onChange) {
52
+ this.onChange = onChange;
53
+ const dir = join(homedir(), ".sanqian", "runtime");
54
+ if (!existsSync(dir)) {
55
+ this.pollInterval = setInterval(() => {
56
+ if (existsSync(dir)) {
57
+ if (this.pollInterval) {
58
+ clearInterval(this.pollInterval);
59
+ this.pollInterval = null;
60
+ }
61
+ this.setupWatcher(dir);
62
+ }
63
+ }, 2e3);
64
+ return;
65
+ }
66
+ this.setupWatcher(dir);
67
+ }
68
+ setupWatcher(dir) {
69
+ try {
70
+ this.watcher = watch(dir, (event, filename) => {
71
+ if (filename === "connection.json") {
72
+ setTimeout(() => {
73
+ const newInfo = this.read();
74
+ const oldInfoStr = JSON.stringify(this.connectionInfo);
75
+ const newInfoStr = JSON.stringify(newInfo);
76
+ if (oldInfoStr !== newInfoStr) {
77
+ this.connectionInfo = newInfo;
78
+ this.onChange?.(newInfo);
79
+ }
80
+ }, 100);
81
+ }
82
+ });
83
+ } catch (e) {
84
+ console.error("Failed to watch connection file:", e);
85
+ }
86
+ }
87
+ /**
88
+ * Stop watching
89
+ */
90
+ stopWatching() {
91
+ if (this.pollInterval) {
92
+ clearInterval(this.pollInterval);
93
+ this.pollInterval = null;
94
+ }
95
+ this.watcher?.close();
96
+ this.watcher = null;
97
+ this.onChange = null;
98
+ }
99
+ /**
100
+ * Check if a process is running by PID
101
+ */
102
+ isProcessRunning(pid) {
103
+ try {
104
+ process.kill(pid, 0);
105
+ return true;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+ /**
111
+ * Get cached connection info (may be stale)
112
+ */
113
+ getCached() {
114
+ return this.connectionInfo;
115
+ }
116
+ /**
117
+ * Build WebSocket URL from connection info
118
+ */
119
+ buildWebSocketUrl(info) {
120
+ return `ws://127.0.0.1:${info.port}${info.ws_path}?token=${info.token}`;
121
+ }
122
+ /**
123
+ * Build HTTP base URL from connection info
124
+ */
125
+ buildHttpUrl(info) {
126
+ return `http://127.0.0.1:${info.port}`;
127
+ }
128
+ /**
129
+ * Find Sanqian executable path
130
+ * Searches in standard installation locations for each platform
131
+ */
132
+ findSanqianPath(customPath) {
133
+ if (customPath) {
134
+ if (existsSync(customPath)) {
135
+ return customPath;
136
+ }
137
+ console.warn(`[SDK] Custom Sanqian path not found: ${customPath}`);
138
+ return null;
139
+ }
140
+ const os = platform();
141
+ const searchPaths = [];
142
+ if (os === "darwin") {
143
+ searchPaths.push(
144
+ // Production: installed app
145
+ "/Applications/Sanqian.app/Contents/MacOS/Sanqian",
146
+ join(homedir(), "Applications/Sanqian.app/Contents/MacOS/Sanqian"),
147
+ // Development: electron-builder output
148
+ join(homedir(), "dev/sanqian/dist/mac-arm64/Sanqian.app/Contents/MacOS/Sanqian"),
149
+ join(homedir(), "dev/sanqian/dist/mac/Sanqian.app/Contents/MacOS/Sanqian")
150
+ );
151
+ } else if (os === "win32") {
152
+ const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
153
+ const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
154
+ const localAppData = process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local");
155
+ searchPaths.push(
156
+ // Production: installed app
157
+ join(programFiles, "Sanqian", "Sanqian.exe"),
158
+ join(programFilesX86, "Sanqian", "Sanqian.exe"),
159
+ join(localAppData, "Programs", "Sanqian", "Sanqian.exe"),
160
+ // Development: electron-builder output
161
+ join(homedir(), "dev", "sanqian", "dist", "win-unpacked", "Sanqian.exe")
162
+ );
163
+ } else {
164
+ searchPaths.push(
165
+ "/usr/bin/sanqian",
166
+ "/usr/local/bin/sanqian",
167
+ join(homedir(), ".local/bin/sanqian"),
168
+ "/opt/Sanqian/sanqian",
169
+ // Development
170
+ join(homedir(), "dev/sanqian/dist/linux-unpacked/sanqian")
171
+ );
172
+ }
173
+ for (const path of searchPaths) {
174
+ if (existsSync(path)) {
175
+ return path;
176
+ }
177
+ }
178
+ return null;
179
+ }
180
+ /**
181
+ * Launch Sanqian in hidden/tray mode
182
+ * Returns true if launch was initiated successfully
183
+ */
184
+ launchSanqian(customPath) {
185
+ const sanqianPath = this.findSanqianPath(customPath);
186
+ if (!sanqianPath) {
187
+ console.error("[SDK] Sanqian executable not found");
188
+ return false;
189
+ }
190
+ console.log(`[SDK] Launching Sanqian from: ${sanqianPath}`);
191
+ try {
192
+ const os = platform();
193
+ if (os === "darwin") {
194
+ const appPath = sanqianPath.replace(
195
+ "/Contents/MacOS/Sanqian",
196
+ ""
197
+ );
198
+ spawn("open", ["-a", appPath, "--args", "--hidden"], {
199
+ detached: true,
200
+ stdio: "ignore"
201
+ }).unref();
202
+ } else {
203
+ spawn(sanqianPath, ["--hidden"], {
204
+ detached: true,
205
+ stdio: "ignore",
206
+ // On Windows, hide the console window
207
+ ...os === "win32" && {
208
+ windowsHide: true,
209
+ shell: false
210
+ }
211
+ }).unref();
212
+ }
213
+ return true;
214
+ } catch (e) {
215
+ console.error("[SDK] Failed to launch Sanqian:", e);
216
+ return false;
217
+ }
218
+ }
219
+ /**
220
+ * Check if Sanqian is running
221
+ */
222
+ isSanqianRunning() {
223
+ return this.read() !== null;
224
+ }
225
+ };
226
+
227
+ // src/client.ts
228
+ var SanqianSDK = class {
229
+ config;
230
+ discovery;
231
+ ws = null;
232
+ connectionInfo = null;
233
+ state = {
234
+ connected: false,
235
+ registering: false,
236
+ registered: false,
237
+ reconnectAttempts: 0
238
+ };
239
+ // Tool handlers by name
240
+ toolHandlers = /* @__PURE__ */ new Map();
241
+ // Pending request futures
242
+ pendingRequests = /* @__PURE__ */ new Map();
243
+ // Timers
244
+ heartbeatTimer = null;
245
+ reconnectTimer = null;
246
+ // Event listeners
247
+ eventListeners = /* @__PURE__ */ new Map();
248
+ constructor(config) {
249
+ this.config = {
250
+ reconnectInterval: 5e3,
251
+ heartbeatInterval: 3e4,
252
+ toolExecutionTimeout: 3e4,
253
+ ...config
254
+ };
255
+ this.discovery = new DiscoveryManager();
256
+ for (const tool of config.tools) {
257
+ this.toolHandlers.set(tool.name, tool.handler);
258
+ }
259
+ }
260
+ // ============================================
261
+ // Lifecycle
262
+ // ============================================
263
+ /**
264
+ * Connect to Sanqian
265
+ *
266
+ * Reads connection info, establishes WebSocket, and registers app.
267
+ * Returns when registration is complete.
268
+ *
269
+ * If autoLaunchSanqian is enabled and Sanqian is not running,
270
+ * SDK will attempt to start it in hidden/tray mode.
271
+ */
272
+ async connect() {
273
+ const info = this.discovery.read();
274
+ if (!info) {
275
+ if (this.config.autoLaunchSanqian) {
276
+ console.log("[SDK] Sanqian not running, attempting to launch...");
277
+ const launched = this.discovery.launchSanqian(this.config.sanqianPath);
278
+ if (launched) {
279
+ console.log("[SDK] Sanqian launch initiated, waiting for startup...");
280
+ } else {
281
+ console.warn("[SDK] Failed to launch Sanqian, will wait for manual start");
282
+ }
283
+ }
284
+ return new Promise((resolve, reject) => {
285
+ console.log("[SDK] Waiting for Sanqian...");
286
+ const timeout = setTimeout(() => {
287
+ this.discovery.stopWatching();
288
+ reject(new Error("Sanqian connection timeout"));
289
+ }, 6e4);
290
+ this.discovery.startWatching(async (newInfo) => {
291
+ if (newInfo) {
292
+ clearTimeout(timeout);
293
+ this.discovery.stopWatching();
294
+ try {
295
+ await this.connectWithInfo(newInfo);
296
+ resolve();
297
+ } catch (e) {
298
+ reject(e);
299
+ }
300
+ }
301
+ });
302
+ });
303
+ }
304
+ await this.connectWithInfo(info);
305
+ }
306
+ /**
307
+ * Connect with known connection info
308
+ */
309
+ async connectWithInfo(info) {
310
+ this.connectionInfo = info;
311
+ const url = this.discovery.buildWebSocketUrl(info);
312
+ return new Promise((resolve, reject) => {
313
+ console.log(`[SDK] Connecting to ${url}`);
314
+ this.ws = new WebSocket(url);
315
+ const connectTimeout = setTimeout(() => {
316
+ reject(new Error("WebSocket connection timeout"));
317
+ this.ws?.close();
318
+ }, 1e4);
319
+ this.ws.on("open", async () => {
320
+ clearTimeout(connectTimeout);
321
+ console.log("[SDK] WebSocket connected");
322
+ this.state.connected = true;
323
+ this.state.reconnectAttempts = 0;
324
+ this.emit("connected");
325
+ try {
326
+ await this.register();
327
+ resolve();
328
+ } catch (e) {
329
+ reject(e);
330
+ }
331
+ });
332
+ this.ws.on("close", (code, reason) => {
333
+ console.log(`[SDK] WebSocket closed: ${code} ${reason.toString()}`);
334
+ this.handleDisconnect(reason.toString());
335
+ });
336
+ this.ws.on("error", (error) => {
337
+ console.error("[SDK] WebSocket error:", error);
338
+ this.state.lastError = error;
339
+ this.emit("error", error);
340
+ });
341
+ this.ws.on("message", (data) => {
342
+ try {
343
+ const message = JSON.parse(data.toString());
344
+ this.handleMessage(message);
345
+ } catch (e) {
346
+ console.error("[SDK] Failed to parse message:", e);
347
+ }
348
+ });
349
+ });
350
+ }
351
+ /**
352
+ * Disconnect from Sanqian
353
+ */
354
+ async disconnect() {
355
+ this.stopHeartbeat();
356
+ this.stopReconnect();
357
+ this.discovery.stopWatching();
358
+ if (this.ws) {
359
+ this.ws.close(1e3, "Client disconnect");
360
+ this.ws = null;
361
+ }
362
+ this.state = {
363
+ connected: false,
364
+ registering: false,
365
+ registered: false,
366
+ reconnectAttempts: 0
367
+ };
368
+ }
369
+ // ============================================
370
+ // Registration
371
+ // ============================================
372
+ async register() {
373
+ this.state.registering = true;
374
+ const msgId = this.generateId();
375
+ const message = {
376
+ id: msgId,
377
+ type: "register",
378
+ app: {
379
+ name: this.config.appName,
380
+ version: this.config.appVersion,
381
+ display_name: this.config.displayName,
382
+ launch_command: this.config.launchCommand
383
+ },
384
+ tools: this.config.tools.map((t) => ({
385
+ name: `${this.config.appName}:${t.name}`,
386
+ description: t.description,
387
+ parameters: t.parameters
388
+ }))
389
+ };
390
+ try {
391
+ const response = await this.sendAndWait(
392
+ message,
393
+ msgId,
394
+ 1e4
395
+ );
396
+ if (!response.success) {
397
+ throw new Error(response.error || "Registration failed");
398
+ }
399
+ this.state.registering = false;
400
+ this.state.registered = true;
401
+ this.startHeartbeat();
402
+ this.emit("registered");
403
+ console.log(`[SDK] Registered as '${this.config.appName}'`);
404
+ } catch (e) {
405
+ this.state.registering = false;
406
+ throw e;
407
+ }
408
+ }
409
+ // ============================================
410
+ // Message Handling
411
+ // ============================================
412
+ handleMessage(message) {
413
+ const { id, type } = message;
414
+ if (id && this.pendingRequests.has(id)) {
415
+ const pending = this.pendingRequests.get(id);
416
+ this.pendingRequests.delete(id);
417
+ pending.resolve(message);
418
+ return;
419
+ }
420
+ switch (type) {
421
+ case "tool_call":
422
+ this.handleToolCall(message);
423
+ break;
424
+ case "heartbeat_ack":
425
+ break;
426
+ case "chat_stream":
427
+ this.handleChatStream(message);
428
+ break;
429
+ default:
430
+ console.warn(`[SDK] Unknown message type: ${type}`);
431
+ }
432
+ }
433
+ handleChatStream(message) {
434
+ const { id, event, content, tool_call, tool_result, conversation_id, usage, error } = message;
435
+ if (!id) return;
436
+ const handler = this.streamHandlers.get(id);
437
+ if (!handler) {
438
+ console.warn(`[SDK] No stream handler for message ${id}`);
439
+ return;
440
+ }
441
+ switch (event) {
442
+ case "text":
443
+ handler.onEvent({ type: "text", content });
444
+ break;
445
+ case "tool_call":
446
+ handler.onEvent({ type: "tool_call", tool_call });
447
+ break;
448
+ case "tool_result":
449
+ if (tool_result) {
450
+ handler.onEvent({
451
+ type: "tool_call",
452
+ tool_call: {
453
+ id: tool_result.call_id,
454
+ type: "function",
455
+ function: {
456
+ name: "tool_result",
457
+ arguments: JSON.stringify(tool_result)
458
+ }
459
+ }
460
+ });
461
+ }
462
+ break;
463
+ case "done":
464
+ handler.onDone({
465
+ message: message.message || { role: "assistant", content: "" },
466
+ conversationId: conversation_id || "",
467
+ usage
468
+ });
469
+ break;
470
+ case "error":
471
+ handler.onError(new Error(error || "Unknown stream error"));
472
+ break;
473
+ }
474
+ }
475
+ async handleToolCall(message) {
476
+ console.log(`[SDK] handleToolCall received:`, JSON.stringify(message));
477
+ const { id, call_id, name, arguments: args } = message;
478
+ const msgId = id || call_id;
479
+ const toolName = name.includes(":") ? name.split(":")[1] : name;
480
+ console.log(`[SDK] Looking for handler: '${toolName}', available handlers:`, Array.from(this.toolHandlers.keys()));
481
+ const handler = this.toolHandlers.get(toolName);
482
+ this.emit("tool_call", { name: toolName, arguments: args });
483
+ if (!handler) {
484
+ await this.sendToolResult(msgId, call_id, false, void 0, `Tool '${toolName}' not found`);
485
+ return;
486
+ }
487
+ try {
488
+ console.log(`[SDK] Executing tool '${toolName}' with args:`, args);
489
+ const result = await Promise.race([
490
+ handler(args),
491
+ this.createTimeout(this.config.toolExecutionTimeout)
492
+ ]);
493
+ console.log(`[SDK] Tool '${toolName}' completed successfully`);
494
+ await this.sendToolResult(msgId, call_id, true, result);
495
+ } catch (e) {
496
+ const error = e instanceof Error ? e.message : String(e);
497
+ console.log(`[SDK] Tool '${toolName}' failed:`, error);
498
+ await this.sendToolResult(msgId, call_id, false, void 0, error);
499
+ }
500
+ }
501
+ async sendToolResult(id, callId, success, result, error) {
502
+ const message = {
503
+ id,
504
+ type: "tool_result",
505
+ call_id: callId,
506
+ success,
507
+ result,
508
+ error
509
+ };
510
+ console.log(`[SDK] Sending tool_result for ${callId}:`, success ? "success" : `error: ${error}`);
511
+ this.send(message);
512
+ }
513
+ // ============================================
514
+ // Connection Management
515
+ // ============================================
516
+ handleDisconnect(reason) {
517
+ this.stopHeartbeat();
518
+ this.state.connected = false;
519
+ this.state.registered = false;
520
+ this.emit("disconnected", reason);
521
+ for (const [, pending] of this.pendingRequests) {
522
+ pending.reject(new Error("Disconnected"));
523
+ }
524
+ this.pendingRequests.clear();
525
+ this.scheduleReconnect();
526
+ }
527
+ scheduleReconnect() {
528
+ if (this.reconnectTimer) return;
529
+ const delay = Math.min(
530
+ 1e3 * Math.pow(2, this.state.reconnectAttempts),
531
+ 3e4
532
+ );
533
+ console.log(`[SDK] Scheduling reconnect in ${delay}ms`);
534
+ this.reconnectTimer = setTimeout(async () => {
535
+ this.reconnectTimer = null;
536
+ this.state.reconnectAttempts++;
537
+ const info = this.discovery.read();
538
+ if (info) {
539
+ try {
540
+ await this.connectWithInfo(info);
541
+ } catch (e) {
542
+ console.error("[SDK] Reconnect failed:", e);
543
+ this.scheduleReconnect();
544
+ }
545
+ } else {
546
+ this.scheduleReconnect();
547
+ }
548
+ }, delay);
549
+ }
550
+ stopReconnect() {
551
+ if (this.reconnectTimer) {
552
+ clearTimeout(this.reconnectTimer);
553
+ this.reconnectTimer = null;
554
+ }
555
+ }
556
+ // ============================================
557
+ // Heartbeat
558
+ // ============================================
559
+ startHeartbeat() {
560
+ this.stopHeartbeat();
561
+ this.heartbeatTimer = setInterval(() => {
562
+ const message = {
563
+ type: "heartbeat",
564
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
565
+ };
566
+ this.send(message);
567
+ }, this.config.heartbeatInterval);
568
+ }
569
+ stopHeartbeat() {
570
+ if (this.heartbeatTimer) {
571
+ clearInterval(this.heartbeatTimer);
572
+ this.heartbeatTimer = null;
573
+ }
574
+ }
575
+ // ============================================
576
+ // Communication
577
+ // ============================================
578
+ send(message) {
579
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
580
+ const data = JSON.stringify(message);
581
+ console.log(`[SDK] WebSocket send:`, data.substring(0, 200));
582
+ this.ws.send(data);
583
+ } else {
584
+ console.error(`[SDK] WebSocket not open, cannot send:`, message);
585
+ }
586
+ }
587
+ sendAndWait(message, id, timeout) {
588
+ return new Promise((resolve, reject) => {
589
+ const timer = setTimeout(() => {
590
+ this.pendingRequests.delete(id);
591
+ reject(new Error("Request timeout"));
592
+ }, timeout);
593
+ this.pendingRequests.set(id, {
594
+ resolve: (value) => {
595
+ clearTimeout(timer);
596
+ resolve(value);
597
+ },
598
+ reject: (error) => {
599
+ clearTimeout(timer);
600
+ reject(error);
601
+ }
602
+ });
603
+ this.send(message);
604
+ });
605
+ }
606
+ // ============================================
607
+ // Public API
608
+ // ============================================
609
+ /**
610
+ * Get current connection state
611
+ */
612
+ getState() {
613
+ return { ...this.state };
614
+ }
615
+ /**
616
+ * Check if connected and registered
617
+ */
618
+ isConnected() {
619
+ return this.state.connected && this.state.registered;
620
+ }
621
+ /**
622
+ * Update tool list dynamically
623
+ */
624
+ async updateTools(tools) {
625
+ this.toolHandlers.clear();
626
+ for (const tool of tools) {
627
+ this.toolHandlers.set(tool.name, tool.handler);
628
+ }
629
+ if (this.isConnected()) {
630
+ const msgId = this.generateId();
631
+ const message = {
632
+ id: msgId,
633
+ type: "tools_update",
634
+ tools: tools.map((t) => ({
635
+ name: `${this.config.appName}:${t.name}`,
636
+ description: t.description,
637
+ parameters: t.parameters
638
+ }))
639
+ };
640
+ await this.sendAndWait(message, msgId, 5e3);
641
+ }
642
+ }
643
+ // ============================================
644
+ // Private Agent API
645
+ // ============================================
646
+ /**
647
+ * Create or update a private agent
648
+ *
649
+ * Private agents are only visible to this SDK app, not in Sanqian UI.
650
+ * If agent with same ID exists, it will be updated.
651
+ *
652
+ * @param config - Agent configuration
653
+ * @returns Full agent info (or agent_id string for backward compatibility)
654
+ */
655
+ async createAgent(config) {
656
+ if (!this.isConnected()) {
657
+ throw new Error("Not connected to Sanqian");
658
+ }
659
+ const msgId = this.generateId();
660
+ const message = {
661
+ id: msgId,
662
+ type: "create_agent",
663
+ agent: config
664
+ };
665
+ const response = await this.sendAndWait(message, msgId, 1e4);
666
+ if (!response.success) {
667
+ throw new Error(response.error || "Failed to create agent");
668
+ }
669
+ if (response.agent) {
670
+ return response.agent;
671
+ }
672
+ return {
673
+ agent_id: response.agent_id,
674
+ name: config.name,
675
+ description: config.description,
676
+ tools: config.tools || []
677
+ };
678
+ }
679
+ /**
680
+ * List all private agents owned by this app
681
+ */
682
+ async listAgents() {
683
+ if (!this.isConnected()) {
684
+ throw new Error("Not connected to Sanqian");
685
+ }
686
+ const msgId = this.generateId();
687
+ const message = {
688
+ id: msgId,
689
+ type: "list_agents"
690
+ };
691
+ const response = await this.sendAndWait(message, msgId, 1e4);
692
+ if (response.error) {
693
+ throw new Error(response.error);
694
+ }
695
+ return response.agents;
696
+ }
697
+ /**
698
+ * Delete a private agent
699
+ */
700
+ async deleteAgent(agentId) {
701
+ if (!this.isConnected()) {
702
+ throw new Error("Not connected to Sanqian");
703
+ }
704
+ const msgId = this.generateId();
705
+ const message = {
706
+ id: msgId,
707
+ type: "delete_agent",
708
+ agent_id: agentId
709
+ };
710
+ const response = await this.sendAndWait(message, msgId, 1e4);
711
+ if (!response.success) {
712
+ throw new Error(response.error || "Failed to delete agent");
713
+ }
714
+ }
715
+ /**
716
+ * Update a private agent
717
+ *
718
+ * Only updates the fields that are provided.
719
+ * @param agentId - Agent ID (short name or full)
720
+ * @param updates - Fields to update
721
+ * @returns Updated agent info
722
+ */
723
+ async updateAgent(agentId, updates) {
724
+ if (!this.isConnected()) {
725
+ throw new Error("Not connected to Sanqian");
726
+ }
727
+ const msgId = this.generateId();
728
+ const message = {
729
+ id: msgId,
730
+ type: "update_agent",
731
+ agent: {
732
+ agent_id: agentId,
733
+ ...updates
734
+ }
735
+ };
736
+ const response = await this.sendAndWait(message, msgId, 1e4);
737
+ if (!response.success || !response.agent) {
738
+ throw new Error(response.error || "Failed to update agent");
739
+ }
740
+ return response.agent;
741
+ }
742
+ // ============================================
743
+ // Conversation API
744
+ // ============================================
745
+ /**
746
+ * List conversations for this app
747
+ */
748
+ async listConversations(options) {
749
+ if (!this.isConnected()) {
750
+ throw new Error("Not connected to Sanqian");
751
+ }
752
+ const msgId = this.generateId();
753
+ const message = {
754
+ id: msgId,
755
+ type: "list_conversations",
756
+ agent_id: options?.agentId,
757
+ limit: options?.limit,
758
+ offset: options?.offset
759
+ };
760
+ const response = await this.sendAndWait(message, msgId, 1e4);
761
+ if (response.error) {
762
+ throw new Error(response.error);
763
+ }
764
+ return {
765
+ conversations: response.conversations,
766
+ total: response.total
767
+ };
768
+ }
769
+ /**
770
+ * Get conversation details with messages
771
+ */
772
+ async getConversation(conversationId, options) {
773
+ if (!this.isConnected()) {
774
+ throw new Error("Not connected to Sanqian");
775
+ }
776
+ const msgId = this.generateId();
777
+ const message = {
778
+ id: msgId,
779
+ type: "get_conversation",
780
+ conversation_id: conversationId,
781
+ include_messages: options?.includeMessages ?? true,
782
+ message_limit: options?.messageLimit,
783
+ message_offset: options?.messageOffset
784
+ };
785
+ const response = await this.sendAndWait(message, msgId, 1e4);
786
+ if (!response.success || !response.conversation) {
787
+ throw new Error(response.error || "Failed to get conversation");
788
+ }
789
+ return response.conversation;
790
+ }
791
+ /**
792
+ * Delete a conversation
793
+ */
794
+ async deleteConversation(conversationId) {
795
+ if (!this.isConnected()) {
796
+ throw new Error("Not connected to Sanqian");
797
+ }
798
+ const msgId = this.generateId();
799
+ const message = {
800
+ id: msgId,
801
+ type: "delete_conversation",
802
+ conversation_id: conversationId
803
+ };
804
+ const response = await this.sendAndWait(message, msgId, 1e4);
805
+ if (!response.success) {
806
+ throw new Error(response.error || "Failed to delete conversation");
807
+ }
808
+ }
809
+ // ============================================
810
+ // Chat API
811
+ // ============================================
812
+ // Pending stream handlers for streaming chat
813
+ streamHandlers = /* @__PURE__ */ new Map();
814
+ /**
815
+ * Send a chat message to an agent (non-streaming)
816
+ *
817
+ * Supports two modes:
818
+ * - Stateless (no conversationId): Caller maintains message history
819
+ * - Stateful (with conversationId): Server stores messages in conversations table
820
+ *
821
+ * @param agentId - Agent ID (short name or full, SDK auto-prefixes)
822
+ * @param messages - Messages to send
823
+ * @param options - Optional settings including conversationId and remoteTools
824
+ * @returns Chat response with assistant message and conversation ID
825
+ */
826
+ async chat(agentId, messages, options) {
827
+ if (!this.isConnected()) {
828
+ throw new Error("Not connected to Sanqian");
829
+ }
830
+ const msgId = this.generateId();
831
+ const message = {
832
+ id: msgId,
833
+ type: "chat",
834
+ agent_id: agentId,
835
+ messages,
836
+ conversation_id: options?.conversationId,
837
+ stream: false,
838
+ remote_tools: options?.remoteTools?.map((t) => ({
839
+ name: t.name,
840
+ description: t.description,
841
+ parameters: t.parameters
842
+ }))
843
+ };
844
+ const response = await this.sendAndWait(message, msgId, 6e5);
845
+ if (!response.success) {
846
+ throw new Error(response.error || "Chat request failed");
847
+ }
848
+ return {
849
+ message: response.message,
850
+ conversationId: response.conversation_id,
851
+ usage: response.usage
852
+ };
853
+ }
854
+ /**
855
+ * Send a chat message to an agent with streaming response
856
+ *
857
+ * Supports two modes:
858
+ * - Stateless (no conversationId): Caller maintains message history
859
+ * - Stateful (with conversationId): Server stores messages in conversations table
860
+ *
861
+ * @param agentId - Agent ID (short name or full, SDK auto-prefixes)
862
+ * @param messages - Messages to send
863
+ * @param options - Optional settings including conversationId and remoteTools
864
+ * @returns AsyncIterable of stream events
865
+ */
866
+ async *chatStream(agentId, messages, options) {
867
+ if (!this.isConnected()) {
868
+ throw new Error("Not connected to Sanqian");
869
+ }
870
+ const msgId = this.generateId();
871
+ const message = {
872
+ id: msgId,
873
+ type: "chat",
874
+ agent_id: agentId,
875
+ messages,
876
+ conversation_id: options?.conversationId,
877
+ stream: true,
878
+ remote_tools: options?.remoteTools?.map((t) => ({
879
+ name: t.name,
880
+ description: t.description,
881
+ parameters: t.parameters
882
+ }))
883
+ };
884
+ const eventQueue = [];
885
+ let done = false;
886
+ let error = null;
887
+ let resolveNext = null;
888
+ this.streamHandlers.set(msgId, {
889
+ onEvent: (event) => {
890
+ eventQueue.push(event);
891
+ resolveNext?.();
892
+ },
893
+ onDone: (response) => {
894
+ eventQueue.push({
895
+ type: "done",
896
+ conversationId: response.conversationId
897
+ });
898
+ done = true;
899
+ resolveNext?.();
900
+ },
901
+ onError: (e) => {
902
+ error = e;
903
+ resolveNext?.();
904
+ }
905
+ });
906
+ try {
907
+ this.send(message);
908
+ while (!done && !error) {
909
+ if (eventQueue.length > 0) {
910
+ yield eventQueue.shift();
911
+ } else {
912
+ await new Promise((resolve) => {
913
+ resolveNext = resolve;
914
+ });
915
+ resolveNext = null;
916
+ }
917
+ }
918
+ while (eventQueue.length > 0) {
919
+ yield eventQueue.shift();
920
+ }
921
+ if (error) {
922
+ throw error;
923
+ }
924
+ } finally {
925
+ this.streamHandlers.delete(msgId);
926
+ }
927
+ }
928
+ /**
929
+ * Start a new conversation with an agent
930
+ *
931
+ * Returns a Conversation object for convenient multi-turn chat.
932
+ *
933
+ * @example
934
+ * ```typescript
935
+ * const conv = sdk.startConversation('assistant')
936
+ * const r1 = await conv.send('Hello')
937
+ * const r2 = await conv.send('Follow up question')
938
+ * console.log(conv.id) // conversation ID
939
+ * ```
940
+ */
941
+ startConversation(agentId) {
942
+ return new Conversation(this, agentId);
943
+ }
944
+ // ============================================
945
+ // Events
946
+ // ============================================
947
+ /**
948
+ * Add event listener
949
+ */
950
+ on(event, listener) {
951
+ if (!this.eventListeners.has(event)) {
952
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
953
+ }
954
+ this.eventListeners.get(event).add(listener);
955
+ return this;
956
+ }
957
+ /**
958
+ * Remove event listener
959
+ */
960
+ off(event, listener) {
961
+ this.eventListeners.get(event)?.delete(listener);
962
+ return this;
963
+ }
964
+ emit(event, ...args) {
965
+ const listeners = this.eventListeners.get(event);
966
+ if (listeners) {
967
+ for (const listener of listeners) {
968
+ try {
969
+ listener(...args);
970
+ } catch (e) {
971
+ console.error(`[SDK] Event listener error for '${event}':`, e);
972
+ }
973
+ }
974
+ }
975
+ }
976
+ // ============================================
977
+ // Utilities
978
+ // ============================================
979
+ generateId() {
980
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
981
+ }
982
+ createTimeout(ms) {
983
+ return new Promise((_, reject) => {
984
+ setTimeout(() => reject(new Error("Timeout")), ms);
985
+ });
986
+ }
987
+ };
988
+ var Conversation = class {
989
+ sdk;
990
+ agentId;
991
+ _conversationId = null;
992
+ constructor(sdk, agentId, conversationId) {
993
+ this.sdk = sdk;
994
+ this.agentId = agentId;
995
+ this._conversationId = conversationId || null;
996
+ }
997
+ /**
998
+ * Get the conversation ID (available after first message)
999
+ */
1000
+ get id() {
1001
+ return this._conversationId;
1002
+ }
1003
+ /**
1004
+ * Send a message and get a response
1005
+ *
1006
+ * First call creates a new conversation, subsequent calls continue it.
1007
+ */
1008
+ async send(content, options) {
1009
+ const response = await this.sdk.chat(
1010
+ this.agentId,
1011
+ [{ role: "user", content }],
1012
+ {
1013
+ conversationId: this._conversationId || void 0,
1014
+ remoteTools: options?.remoteTools
1015
+ }
1016
+ );
1017
+ if (response.conversationId && !this._conversationId) {
1018
+ this._conversationId = response.conversationId;
1019
+ }
1020
+ return response;
1021
+ }
1022
+ /**
1023
+ * Send a message with streaming response
1024
+ */
1025
+ async *sendStream(content, options) {
1026
+ const stream = this.sdk.chatStream(
1027
+ this.agentId,
1028
+ [{ role: "user", content }],
1029
+ {
1030
+ conversationId: this._conversationId || void 0,
1031
+ remoteTools: options?.remoteTools
1032
+ }
1033
+ );
1034
+ for await (const event of stream) {
1035
+ if (event.type === "done" && event.conversationId && !this._conversationId) {
1036
+ this._conversationId = event.conversationId;
1037
+ }
1038
+ yield event;
1039
+ }
1040
+ }
1041
+ /**
1042
+ * Delete this conversation
1043
+ */
1044
+ async delete() {
1045
+ if (!this._conversationId) {
1046
+ throw new Error("No conversation to delete");
1047
+ }
1048
+ await this.sdk.deleteConversation(this._conversationId);
1049
+ this._conversationId = null;
1050
+ }
1051
+ /**
1052
+ * Get conversation details including message history
1053
+ */
1054
+ async getDetails(options) {
1055
+ if (!this._conversationId) {
1056
+ throw new Error("No conversation to get details for");
1057
+ }
1058
+ return this.sdk.getConversation(this._conversationId, {
1059
+ includeMessages: true,
1060
+ messageLimit: options?.messageLimit
1061
+ });
1062
+ }
1063
+ };
1064
+ export {
1065
+ Conversation,
1066
+ DiscoveryManager,
1067
+ SanqianSDK
1068
+ };
1069
+ //# sourceMappingURL=index.mjs.map