@sonzai-labs/agents 1.0.0 → 1.0.1

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 CHANGED
@@ -1,9 +1,9 @@
1
1
  # Sonzai TypeScript SDK
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@sonzai/sdk.svg)](https://www.npmjs.com/package/@sonzai/sdk)
3
+ [![npm version](https://img.shields.io/npm/v/@sonzai-labs/agents.svg)](https://www.npmjs.com/package/@sonzai-labs/agents)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- The official TypeScript SDK for the [Sonzai Character Engine API](https://sonz.ai). Build AI characters with persistent memory, evolving personality, and proactive behaviors.
6
+ The official TypeScript SDK for the [Sonzai Mind Layer API](https://sonz.ai). Build AI agents with persistent memory, evolving personality, and proactive behaviors.
7
7
 
8
8
  **Zero runtime dependencies.** Uses the native `fetch` API. Works with Node.js (>=18), Bun, and Deno.
9
9
 
@@ -11,19 +11,19 @@ The official TypeScript SDK for the [Sonzai Character Engine API](https://sonz.a
11
11
 
12
12
  ```bash
13
13
  # npm
14
- npm install @sonzai/sdk
14
+ npm install @sonzai-labs/agents
15
15
 
16
16
  # bun
17
- bun add @sonzai/sdk
17
+ bun add @sonzai-labs/agents
18
18
 
19
19
  # deno
20
- import { Sonzai } from "npm:@sonzai/sdk";
20
+ import { Sonzai } from "npm:@sonzai-labs/agents";
21
21
  ```
22
22
 
23
23
  ## Quick Start
24
24
 
25
25
  ```ts
26
- import { Sonzai } from "@sonzai/sdk";
26
+ import { Sonzai } from "@sonzai-labs/agents";
27
27
 
28
28
  const client = new Sonzai({ apiKey: "your-api-key" });
29
29
 
@@ -371,7 +371,7 @@ import {
371
371
  BadRequestError,
372
372
  RateLimitError,
373
373
  SonzaiError,
374
- } from "@sonzai/sdk";
374
+ } from "@sonzai-labs/agents";
375
375
 
376
376
  try {
377
377
  const res = await client.agents.chat("agent-id", { messages: [...] });
package/dist/index.cjs CHANGED
@@ -32,9 +32,11 @@ var PermissionDeniedError = class extends SonzaiError {
32
32
  }
33
33
  };
34
34
  var RateLimitError = class extends SonzaiError {
35
- constructor(message) {
35
+ retryAfter;
36
+ constructor(message, retryAfter) {
36
37
  super(message);
37
38
  this.name = "RateLimitError";
39
+ this.retryAfter = retryAfter;
38
40
  }
39
41
  };
40
42
  var InternalServerError = class extends SonzaiError {
@@ -63,6 +65,7 @@ var HTTPClient = class {
63
65
  baseUrl;
64
66
  headers;
65
67
  timeout;
68
+ maxRetries;
66
69
  constructor(options) {
67
70
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
68
71
  this.headers = {
@@ -71,27 +74,53 @@ var HTTPClient = class {
71
74
  "User-Agent": "sonzai-typescript/1.13.0"
72
75
  };
73
76
  this.timeout = options.timeout;
77
+ this.maxRetries = options.maxRetries;
74
78
  }
75
79
  async request(method, path, options) {
76
80
  const url = this.buildUrl(path, options?.params);
77
- const controller = new AbortController();
78
- const timer = setTimeout(() => controller.abort(), this.timeout);
79
- try {
80
- const response = await fetch(url, {
81
- method,
82
- headers: this.headers,
83
- body: options?.body ? JSON.stringify(options.body) : void 0,
84
- signal: controller.signal
85
- });
86
- await this.throwOnError(response);
87
- const contentType = response.headers.get("content-type") ?? "";
88
- if (contentType.includes("application/json")) {
89
- return await response.json();
81
+ const isIdempotent = method === "GET" || method === "DELETE";
82
+ const maxAttempts = isIdempotent ? this.maxRetries + 1 : 1;
83
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
84
+ const controller = new AbortController();
85
+ const timer = setTimeout(() => controller.abort(), this.timeout);
86
+ try {
87
+ const response = await fetch(url, {
88
+ method,
89
+ headers: this.headers,
90
+ body: options?.body ? JSON.stringify(options.body) : void 0,
91
+ signal: controller.signal
92
+ });
93
+ if (response.status >= 500 && isIdempotent && attempt < maxAttempts - 1) {
94
+ clearTimeout(timer);
95
+ await this.backoff(attempt);
96
+ continue;
97
+ }
98
+ await this.throwOnError(response);
99
+ const contentType = response.headers.get("content-type") ?? "";
100
+ if (contentType.includes("application/json")) {
101
+ return await response.json();
102
+ }
103
+ return await response.text();
104
+ } catch (error) {
105
+ clearTimeout(timer);
106
+ if (isIdempotent && attempt < maxAttempts - 1 && this.isNetworkError(error)) {
107
+ await this.backoff(attempt);
108
+ continue;
109
+ }
110
+ throw error;
111
+ } finally {
112
+ clearTimeout(timer);
90
113
  }
91
- return await response.text();
92
- } finally {
93
- clearTimeout(timer);
94
114
  }
115
+ throw new InternalServerError("Max retries exceeded");
116
+ }
117
+ async backoff(attempt) {
118
+ const delay = Math.min(1e3 * 2 ** attempt, 1e4) + Math.random() * 500;
119
+ await new Promise((resolve) => setTimeout(resolve, delay));
120
+ }
121
+ isNetworkError(error) {
122
+ if (error instanceof SonzaiError) return false;
123
+ return error instanceof TypeError || error instanceof DOMException && error.name === "AbortError";
95
124
  }
96
125
  async get(path, params) {
97
126
  return this.request("GET", path, { params });
@@ -129,33 +158,47 @@ var HTTPClient = class {
129
158
  const reader = response.body.getReader();
130
159
  const decoder = new TextDecoder();
131
160
  let buffer = "";
132
- while (true) {
133
- const { done, value } = await reader.read();
134
- if (done) break;
135
- buffer += decoder.decode(value, { stream: true });
136
- const lines = buffer.split("\n");
137
- buffer = lines.pop() ?? "";
138
- for (const line of lines) {
139
- const trimmed = line.trim();
140
- if (!trimmed) continue;
161
+ try {
162
+ while (true) {
163
+ const { done, value } = await reader.read();
164
+ if (done) break;
165
+ buffer += decoder.decode(value, { stream: true });
166
+ const lines = buffer.split("\n");
167
+ buffer = lines.pop() ?? "";
168
+ for (const line of lines) {
169
+ const trimmed = line.trim();
170
+ if (!trimmed) continue;
171
+ if (trimmed === "data: [DONE]") return;
172
+ if (trimmed.startsWith("data: ")) {
173
+ try {
174
+ yield JSON.parse(trimmed.slice(6));
175
+ } catch (e) {
176
+ console.warn(
177
+ "[sonzai-sdk] Malformed SSE JSON event skipped:",
178
+ trimmed.slice(6),
179
+ e
180
+ );
181
+ }
182
+ }
183
+ }
184
+ }
185
+ if (buffer.trim()) {
186
+ const trimmed = buffer.trim();
141
187
  if (trimmed === "data: [DONE]") return;
142
188
  if (trimmed.startsWith("data: ")) {
143
189
  try {
144
190
  yield JSON.parse(trimmed.slice(6));
145
- } catch {
191
+ } catch (e) {
192
+ console.warn(
193
+ "[sonzai-sdk] Malformed SSE JSON event skipped:",
194
+ trimmed.slice(6),
195
+ e
196
+ );
146
197
  }
147
198
  }
148
199
  }
149
- }
150
- if (buffer.trim()) {
151
- const trimmed = buffer.trim();
152
- if (trimmed === "data: [DONE]") return;
153
- if (trimmed.startsWith("data: ")) {
154
- try {
155
- yield JSON.parse(trimmed.slice(6));
156
- } catch {
157
- }
158
- }
200
+ } finally {
201
+ await reader.cancel();
159
202
  }
160
203
  } finally {
161
204
  clearTimeout(timer);
@@ -195,8 +238,14 @@ var HTTPClient = class {
195
238
  throw new NotFoundError(message);
196
239
  case 400:
197
240
  throw new BadRequestError(message);
198
- case 429:
199
- throw new RateLimitError(message);
241
+ case 429: {
242
+ const retryAfterRaw = response.headers.get("Retry-After");
243
+ const retryAfter = retryAfterRaw ? Number(retryAfterRaw) : void 0;
244
+ throw new RateLimitError(
245
+ message,
246
+ retryAfter && !Number.isNaN(retryAfter) ? retryAfter : void 0
247
+ );
248
+ }
200
249
  default:
201
250
  if (status >= 500) throw new InternalServerError(message);
202
251
  throw new APIError(status, message);
@@ -793,72 +842,42 @@ var Voice = class {
793
842
  constructor(http) {
794
843
  this.http = http;
795
844
  }
796
- /** Convert text to speech using the agent's voice. */
797
- async textToSpeech(agentId, options) {
798
- const body = { text: options.text };
799
- if (options.voiceName) body.voice_name = options.voiceName;
800
- if (options.language) body.language = options.language;
801
- if (options.emotionalContext)
802
- body.emotional_context = options.emotionalContext;
803
- return this.http.post(
804
- `/api/v1/agents/${agentId}/voice/tts`,
805
- body
806
- );
807
- }
808
- /** Find the best matching voice for an agent based on personality. */
809
- async voiceMatch(agentId, options = {}) {
810
- const body = {};
811
- if (options.big5) body.big5 = options.big5;
812
- if (options.preferredGender)
813
- body.preferred_gender = options.preferredGender;
814
- return this.http.post(
815
- `/api/v1/agents/${agentId}/voice/match`,
816
- body
817
- );
818
- }
819
- /** Perform a single-turn voice chat: send audio, receive text + audio response. */
820
- async voiceChat(agentId, options) {
821
- const body = { audio: options.audio };
822
- if (options.userId) body.user_id = options.userId;
823
- if (options.audioFormat) body.audio_format = options.audioFormat;
824
- if (options.voiceName) body.voice_name = options.voiceName;
825
- if (options.continuationToken)
826
- body.continuation_token = options.continuationToken;
827
- if (options.language) body.language = options.language;
828
- return this.http.post(
829
- `/api/v1/agents/${agentId}/voice/chat`,
830
- body
831
- );
832
- }
833
845
  /**
834
- * Get a short-lived token for WebSocket voice streaming.
846
+ * Get a short-lived token for voice live WebSocket streaming.
835
847
  * The token expires in 60 seconds and is single-use.
836
848
  */
837
849
  async getToken(agentId, options = {}) {
838
850
  const body = {};
839
851
  if (options.voiceName) body.voiceName = options.voiceName;
840
852
  if (options.language) body.language = options.language;
841
- if (options.entityContext) body.entityContext = options.entityContext;
853
+ if (options.userId) body.userId = options.userId;
854
+ if (options.compiledSystemPrompt)
855
+ body.compiledSystemPrompt = options.compiledSystemPrompt;
842
856
  return this.http.post(
843
- `/api/v1/agents/${agentId}/voice/ws-token`,
857
+ `/api/v1/agents/${agentId}/voice/live-ws-token`,
844
858
  body
845
859
  );
846
860
  }
847
861
  /**
848
- * Open a bidirectional WebSocket for real-time voice chat.
862
+ * Open a bidirectional WebSocket for real-time voice chat via Gemini Live.
849
863
  *
850
864
  * @example
851
865
  * ```ts
852
866
  * const token = await client.agents.voice.getToken(agentId);
853
867
  * const stream = await client.agents.voice.stream(token);
854
868
  *
869
+ * // Send PCM audio chunks (16kHz, 16-bit, mono)
855
870
  * stream.sendAudio(audioChunk);
856
- * stream.endOfSpeech();
871
+ *
872
+ * // Or send text input instead of audio
873
+ * stream.sendText("Hello!");
857
874
  *
858
875
  * for await (const event of stream) {
859
- * if (event.type === "transcript") console.log("User:", event.text);
860
- * if (event.type === "audio") playAudio(event.audio);
861
- * if (event.type === "turn_complete") break;
876
+ * if (event.type === "input_transcript") console.log("User:", event.text);
877
+ * if (event.type === "output_transcript") console.log("Agent:", event.text);
878
+ * if (event.type === "audio") playPCMAudio(event.audio); // 24kHz PCM
879
+ * if (event.type === "turn_complete") console.log("Turn done");
880
+ * if (event.type === "session_ended") break;
862
881
  * }
863
882
  *
864
883
  * stream.close();
@@ -884,13 +903,25 @@ var VoiceStreamInstance = class _VoiceStreamInstance {
884
903
  const parsed = JSON.parse(evt.data);
885
904
  event = {
886
905
  type: parsed.type ?? "",
887
- sessionId: parsed.session_id,
888
- speaking: parsed.speaking,
906
+ sessionId: parsed.sessionId ?? parsed.session_id,
889
907
  text: parsed.text,
890
- continuationToken: parsed.continuation_token,
891
- contentType: parsed.content_type,
908
+ isFinal: parsed.isFinal,
909
+ speaking: parsed.speaking,
910
+ turnIndex: parsed.turnIndex,
911
+ name: parsed.name,
912
+ status: parsed.status,
913
+ facts: parsed.facts,
914
+ emotions: parsed.emotions,
915
+ relationshipDelta: parsed.relationshipDelta,
916
+ promptTokens: parsed.promptTokens,
917
+ completionTokens: parsed.completionTokens,
918
+ totalTokens: parsed.totalTokens,
919
+ reason: parsed.reason,
920
+ totalUsage: parsed.totalUsage,
921
+ turnCount: parsed.turnCount,
922
+ voiceName: parsed.voiceName,
892
923
  error: parsed.error,
893
- errorCode: parsed.error_code
924
+ errorCode: parsed.errorCode ?? parsed.error_code ?? parsed.code
894
925
  };
895
926
  }
896
927
  if (this.waitResolve) {
@@ -944,16 +975,19 @@ var VoiceStreamInstance = class _VoiceStreamInstance {
944
975
  sendAudio(audio) {
945
976
  this.ws.send(audio);
946
977
  }
947
- /** Signal the server that the user has finished speaking. */
948
- endOfSpeech() {
949
- this.ws.send(JSON.stringify({ type: "end_of_speech" }));
978
+ /** Send a text message to the agent instead of audio. */
979
+ sendText(text) {
980
+ this.ws.send(JSON.stringify({ type: "text_input", text }));
950
981
  }
951
- /** Change audio format, voice, or language mid-session. */
982
+ /** Gracefully end the voice session. */
983
+ endSession() {
984
+ this.ws.send(JSON.stringify({ type: "end_session" }));
985
+ }
986
+ /** Change audio format or sample rate mid-session. */
952
987
  configure(options) {
953
988
  const msg = { type: "config" };
954
- if (options.audioFormat) msg.audio_format = options.audioFormat;
955
- if (options.voiceName) msg.voice_name = options.voiceName;
956
- if (options.language) msg.language = options.language;
989
+ if (options.audioFormat) msg.audioFormat = options.audioFormat;
990
+ if (options.sampleRate) msg.sampleRate = options.sampleRate;
957
991
  this.ws.send(JSON.stringify(msg));
958
992
  }
959
993
  /** Close the voice stream. */
@@ -1003,6 +1037,11 @@ var Voices = class {
1003
1037
  };
1004
1038
 
1005
1039
  // src/resources/agents.ts
1040
+ function requireNonEmpty(value, name) {
1041
+ if (!value || typeof value !== "string" || value.trim() === "") {
1042
+ throw new Error(`${name} must be a non-empty string`);
1043
+ }
1044
+ }
1006
1045
  var Agents = class {
1007
1046
  constructor(http) {
1008
1047
  this.http = http;
@@ -1072,10 +1111,12 @@ var Agents = class {
1072
1111
  }
1073
1112
  /** Get an agent by ID. */
1074
1113
  async get(agentId) {
1114
+ requireNonEmpty(agentId, "agentId");
1075
1115
  return this.http.get(`/api/v1/agents/${agentId}`);
1076
1116
  }
1077
1117
  /** Update an agent's profile. */
1078
1118
  async update(agentId, options) {
1119
+ requireNonEmpty(agentId, "agentId");
1079
1120
  const body = {};
1080
1121
  if (options.name) body.name = options.name;
1081
1122
  if (options.bio) body.bio = options.bio;
@@ -1093,6 +1134,7 @@ var Agents = class {
1093
1134
  }
1094
1135
  /** Delete an agent. */
1095
1136
  async delete(agentId) {
1137
+ requireNonEmpty(agentId, "agentId");
1096
1138
  await this.http.delete(`/api/v1/agents/${agentId}`);
1097
1139
  }
1098
1140
  // -- Chat --
@@ -1115,6 +1157,7 @@ var Agents = class {
1115
1157
  }
1116
1158
  /** Send a chat message and stream events as an async iterator. */
1117
1159
  async *chatStream(options) {
1160
+ requireNonEmpty(options.agent, "agentId");
1118
1161
  const body = this.buildChatBody(options);
1119
1162
  for await (const event of this.http.streamSSE(
1120
1163
  "POST",