@spekoai/sdk 0.5.1 → 0.5.2

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +88 -0
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1 -0
  6. package/dist/lib/client.d.ts +13 -1
  7. package/dist/lib/client.d.ts.map +1 -1
  8. package/dist/lib/client.js +22 -2
  9. package/dist/lib/http.d.ts +11 -3
  10. package/dist/lib/http.d.ts.map +1 -1
  11. package/dist/lib/http.js +29 -12
  12. package/dist/lib/resources/agents.js +1 -0
  13. package/dist/lib/resources/calls.d.ts +19 -1
  14. package/dist/lib/resources/calls.d.ts.map +1 -1
  15. package/dist/lib/resources/calls.js +22 -0
  16. package/dist/lib/resources/phone-numbers.d.ts +2 -1
  17. package/dist/lib/resources/phone-numbers.d.ts.map +1 -1
  18. package/dist/lib/resources/phone-numbers.js +2 -1
  19. package/dist/lib/resources/realtime.d.ts +3 -5
  20. package/dist/lib/resources/realtime.d.ts.map +1 -1
  21. package/dist/lib/resources/realtime.js +829 -91
  22. package/dist/lib/resources/sessions.d.ts +53 -0
  23. package/dist/lib/resources/sessions.d.ts.map +1 -0
  24. package/dist/lib/resources/sessions.js +166 -0
  25. package/dist/lib/resources/sms.d.ts +80 -0
  26. package/dist/lib/resources/sms.d.ts.map +1 -0
  27. package/dist/lib/resources/sms.js +152 -0
  28. package/dist/lib/resources/transcribe.d.ts.map +1 -1
  29. package/dist/lib/resources/transcribe.js +5 -2
  30. package/dist/lib/resources/voice.d.ts +283 -1
  31. package/dist/lib/resources/voice.d.ts.map +1 -1
  32. package/dist/lib/resources/voice.js +345 -0
  33. package/dist/lib/resources/webhooks.d.ts +25 -0
  34. package/dist/lib/resources/webhooks.d.ts.map +1 -0
  35. package/dist/lib/resources/webhooks.js +46 -0
  36. package/dist/lib/types/index.d.ts +944 -9
  37. package/dist/lib/types/index.d.ts.map +1 -1
  38. package/dist/lib/voice-contract.d.ts +280 -0
  39. package/dist/lib/voice-contract.d.ts.map +1 -0
  40. package/dist/lib/voice-contract.js +115 -0
  41. package/package.json +2 -1
  42. package/src/index.ts +212 -0
  43. package/src/lib/client.ts +169 -0
  44. package/src/lib/errors.ts +28 -0
  45. package/src/lib/http.ts +442 -0
  46. package/src/lib/resources/agents.ts +211 -0
  47. package/src/lib/resources/callbacks.ts +40 -0
  48. package/src/lib/resources/calls.ts +113 -0
  49. package/src/lib/resources/complete.ts +63 -0
  50. package/src/lib/resources/credits.ts +41 -0
  51. package/src/lib/resources/knowledge-bases.ts +199 -0
  52. package/src/lib/resources/phone-numbers.ts +109 -0
  53. package/src/lib/resources/realtime-globals.d.ts +31 -0
  54. package/src/lib/resources/realtime.spec.ts +565 -0
  55. package/src/lib/resources/realtime.ts +1169 -0
  56. package/src/lib/resources/sessions.ts +191 -0
  57. package/src/lib/resources/sms.ts +214 -0
  58. package/src/lib/resources/synthesize.ts +101 -0
  59. package/src/lib/resources/transcribe.ts +91 -0
  60. package/src/lib/resources/usage.ts +24 -0
  61. package/src/lib/resources/voice.ts +426 -0
  62. package/src/lib/resources/voices.ts +32 -0
  63. package/src/lib/resources/webhooks.ts +67 -0
  64. package/src/lib/types/index.ts +2409 -0
  65. package/src/lib/voice-contract.ts +358 -0
@@ -0,0 +1,442 @@
1
+ import { SpekoApiError, SpekoAuthError, SpekoRateLimitError } from './errors.js';
2
+
3
+ export interface HttpClientOptions {
4
+ baseUrl: string;
5
+ apiKey: string;
6
+ timeout: number;
7
+ }
8
+
9
+ const USER_AGENT = '@spekoai/sdk/0.5.2';
10
+
11
+ export class HttpClient {
12
+ private readonly baseUrl: string;
13
+ private readonly authHeader: string;
14
+ private readonly jsonHeaders: Record<string, string>;
15
+ private readonly timeout: number;
16
+
17
+ constructor(options: HttpClientOptions) {
18
+ this.baseUrl = options.baseUrl.replace(/\/$/, '');
19
+ this.authHeader = `Bearer ${options.apiKey}`;
20
+ this.jsonHeaders = {
21
+ Authorization: this.authHeader,
22
+ 'Content-Type': 'application/json',
23
+ 'User-Agent': USER_AGENT,
24
+ };
25
+ this.timeout = options.timeout;
26
+ }
27
+
28
+ async request<T>(
29
+ method: string,
30
+ path: string,
31
+ body?: unknown,
32
+ externalSignal?: AbortSignal,
33
+ extraHeaders?: Record<string, string>,
34
+ ): Promise<T> {
35
+ const url = `${this.baseUrl}${path}`;
36
+ const { signal, cleanup } = this.buildSignal(externalSignal);
37
+
38
+ try {
39
+ const response = await fetch(url, {
40
+ method,
41
+ headers: { ...this.jsonHeaders, ...extraHeaders },
42
+ body: body ? JSON.stringify(body) : undefined,
43
+ signal,
44
+ });
45
+
46
+ if (!response.ok) {
47
+ await this.handleError(response);
48
+ }
49
+
50
+ if (response.status === 204) return undefined as T;
51
+ return (await response.json()) as T;
52
+ } finally {
53
+ cleanup();
54
+ }
55
+ }
56
+
57
+ async get<T>(path: string, externalSignal?: AbortSignal): Promise<T> {
58
+ return this.request<T>('GET', path, undefined, externalSignal);
59
+ }
60
+
61
+ async post<T>(
62
+ path: string,
63
+ body: unknown,
64
+ externalSignal?: AbortSignal,
65
+ extraHeaders?: Record<string, string>,
66
+ ): Promise<T> {
67
+ return this.request<T>('POST', path, body, externalSignal, extraHeaders);
68
+ }
69
+
70
+ async put<T>(path: string, body: unknown, externalSignal?: AbortSignal): Promise<T> {
71
+ return this.request<T>('PUT', path, body, externalSignal);
72
+ }
73
+
74
+ async delete<T>(path: string, externalSignal?: AbortSignal): Promise<T> {
75
+ return this.request<T>('DELETE', path, undefined, externalSignal);
76
+ }
77
+
78
+ async patch<T>(path: string, body: unknown, externalSignal?: AbortSignal): Promise<T> {
79
+ return this.request<T>('PATCH', path, body, externalSignal);
80
+ }
81
+
82
+ /**
83
+ * Send raw bytes as the request body and parse a JSON response.
84
+ * Used by `speko.transcribe()` to upload audio and receive a transcript.
85
+ */
86
+ async requestRaw<T>(
87
+ method: string,
88
+ path: string,
89
+ bodyBytes: Uint8Array,
90
+ extraHeaders: Record<string, string>,
91
+ externalSignal?: AbortSignal,
92
+ ): Promise<T> {
93
+ const url = `${this.baseUrl}${path}`;
94
+ const { signal, cleanup } = this.buildSignal(externalSignal);
95
+
96
+ try {
97
+ const headers: Record<string, string> = {
98
+ Authorization: this.authHeader,
99
+ 'User-Agent': USER_AGENT,
100
+ ...extraHeaders,
101
+ };
102
+
103
+ const response = await fetch(url, {
104
+ method,
105
+ headers,
106
+ body: bodyBytes as unknown as string,
107
+ signal,
108
+ });
109
+
110
+ if (!response.ok) {
111
+ await this.handleError(response);
112
+ }
113
+
114
+ return (await response.json()) as T;
115
+ } finally {
116
+ cleanup();
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Send JSON and receive a binary response (e.g. synthesized audio).
122
+ * Returns the raw bytes plus the response headers so callers can read
123
+ * `Content-Type`, `X-Speko-Provider`, etc.
124
+ */
125
+ async requestBinary(
126
+ method: string,
127
+ path: string,
128
+ body: unknown,
129
+ externalSignal?: AbortSignal,
130
+ ): Promise<{ bytes: Uint8Array; headers: Record<string, string> }> {
131
+ const url = `${this.baseUrl}${path}`;
132
+ const { signal, cleanup } = this.buildSignal(externalSignal);
133
+
134
+ try {
135
+ const response = await fetch(url, {
136
+ method,
137
+ headers: this.jsonHeaders,
138
+ body: body ? JSON.stringify(body) : undefined,
139
+ signal,
140
+ });
141
+
142
+ if (!response.ok) {
143
+ await this.handleError(response);
144
+ }
145
+
146
+ const buffer = await response.arrayBuffer();
147
+ const headers: Record<string, string> = {};
148
+ response.headers.forEach((value, key) => {
149
+ headers[key] = value;
150
+ });
151
+
152
+ return { bytes: new Uint8Array(buffer), headers };
153
+ } finally {
154
+ cleanup();
155
+ }
156
+ }
157
+
158
+ async *requestSse(
159
+ method: string,
160
+ path: string,
161
+ body: unknown,
162
+ externalSignal?: AbortSignal,
163
+ extraHeaders?: Record<string, string>,
164
+ /**
165
+ * Per-request override of the client timeout. Long-lived SSE responses
166
+ * (session observation) outlive the default 30 s, which would abort them
167
+ * mid-stream.
168
+ */
169
+ timeoutMs?: number,
170
+ ): AsyncIterableIterator<{ event: string; id?: string; data: unknown }> {
171
+ const url = `${this.baseUrl}${path}`;
172
+ const { signal, cleanup } = this.buildSignal(externalSignal, timeoutMs);
173
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
174
+
175
+ try {
176
+ const response = await fetch(url, {
177
+ method,
178
+ headers: { ...this.jsonHeaders, ...extraHeaders },
179
+ body: body ? JSON.stringify(body) : undefined,
180
+ signal,
181
+ });
182
+
183
+ if (!response.ok) {
184
+ await this.handleError(response);
185
+ }
186
+
187
+ if (!response.body) {
188
+ throw new SpekoApiError('Response body is empty', response.status, 'EMPTY_BODY');
189
+ }
190
+
191
+ reader = response.body.getReader();
192
+ const decoder = new TextDecoder();
193
+ let buffer = '';
194
+
195
+ while (true) {
196
+ const { done, value } = await reader.read();
197
+ if (done) break;
198
+ buffer += decoder.decode(value, { stream: true });
199
+ const events = drainSseEvents(buffer);
200
+ buffer = events.remainder;
201
+ for (const event of events.items) {
202
+ yield event;
203
+ }
204
+ }
205
+
206
+ buffer += decoder.decode();
207
+ const events = drainSseEvents(buffer + '\n\n');
208
+ for (const event of events.items) {
209
+ yield event;
210
+ }
211
+ } finally {
212
+ reader?.releaseLock();
213
+ cleanup();
214
+ }
215
+ }
216
+
217
+ async requestRawSse(
218
+ method: string,
219
+ path: string,
220
+ bodyBytes: Uint8Array,
221
+ extraHeaders: Record<string, string>,
222
+ externalSignal?: AbortSignal,
223
+ ): Promise<AsyncIterableIterator<{ event: string; id?: string; data: unknown }>> {
224
+ const url = `${this.baseUrl}${path}`;
225
+ const { signal, cleanup } = this.buildSignal(externalSignal);
226
+
227
+ const response = await fetch(url, {
228
+ method,
229
+ headers: {
230
+ Authorization: this.authHeader,
231
+ 'User-Agent': USER_AGENT,
232
+ ...extraHeaders,
233
+ },
234
+ body: bodyBytes as unknown as string,
235
+ signal,
236
+ });
237
+
238
+ if (!response.ok) {
239
+ cleanup();
240
+ await this.handleError(response);
241
+ }
242
+
243
+ if (!response.body) {
244
+ cleanup();
245
+ throw new SpekoApiError('Response body is empty', response.status, 'EMPTY_BODY');
246
+ }
247
+
248
+ return this.readSseBody(response.body, cleanup);
249
+ }
250
+
251
+ async requestBinaryStream(
252
+ method: string,
253
+ path: string,
254
+ body: unknown,
255
+ externalSignal?: AbortSignal,
256
+ extraHeaders?: Record<string, string>,
257
+ ): Promise<{ chunks: AsyncIterableIterator<Uint8Array>; headers: Record<string, string> }> {
258
+ const url = `${this.baseUrl}${path}`;
259
+ const { signal, cleanup } = this.buildSignal(externalSignal);
260
+
261
+ const response = await fetch(url, {
262
+ method,
263
+ headers: { ...this.jsonHeaders, ...extraHeaders },
264
+ body: body ? JSON.stringify(body) : undefined,
265
+ signal,
266
+ });
267
+
268
+ if (!response.ok) {
269
+ cleanup();
270
+ await this.handleError(response);
271
+ }
272
+
273
+ if (!response.body) {
274
+ cleanup();
275
+ throw new SpekoApiError('Response body is empty', response.status, 'EMPTY_BODY');
276
+ }
277
+
278
+ const headers: Record<string, string> = {};
279
+ response.headers.forEach((value, key) => {
280
+ headers[key] = value;
281
+ });
282
+
283
+ return {
284
+ headers,
285
+ chunks: this.readBinaryBody(response.body, cleanup),
286
+ };
287
+ }
288
+
289
+ private async *readSseBody(
290
+ body: ReadableStream<Uint8Array>,
291
+ cleanup: () => void,
292
+ ): AsyncIterableIterator<{ event: string; id?: string; data: unknown }> {
293
+ const reader = body.getReader();
294
+ const decoder = new TextDecoder();
295
+ let buffer = '';
296
+
297
+ try {
298
+ while (true) {
299
+ const { done, value } = await reader.read();
300
+ if (done) break;
301
+ buffer += decoder.decode(value, { stream: true });
302
+ const events = drainSseEvents(buffer);
303
+ buffer = events.remainder;
304
+ for (const event of events.items) {
305
+ yield event;
306
+ }
307
+ }
308
+
309
+ buffer += decoder.decode();
310
+ const events = drainSseEvents(buffer + '\n\n');
311
+ for (const event of events.items) {
312
+ yield event;
313
+ }
314
+ } finally {
315
+ reader.releaseLock();
316
+ cleanup();
317
+ }
318
+ }
319
+
320
+ private async *readBinaryBody(
321
+ body: ReadableStream<Uint8Array>,
322
+ cleanup: () => void,
323
+ ): AsyncIterableIterator<Uint8Array> {
324
+ const reader = body.getReader();
325
+ try {
326
+ while (true) {
327
+ const { done, value } = await reader.read();
328
+ if (done) break;
329
+ yield value;
330
+ }
331
+ } finally {
332
+ reader.releaseLock();
333
+ cleanup();
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Compose the internal timeout signal with an optional external signal so
339
+ * that callers can cancel in-flight requests (e.g. LiveKit Agents tearing
340
+ * down a session) while still enforcing the client's configured timeout.
341
+ */
342
+ private buildSignal(
343
+ externalSignal?: AbortSignal,
344
+ timeoutMs?: number,
345
+ ): {
346
+ signal: AbortSignal;
347
+ cleanup: () => void;
348
+ } {
349
+ const controller = new AbortController();
350
+ const effectiveTimeout = timeoutMs ?? this.timeout;
351
+ const timer =
352
+ effectiveTimeout > 0 ? setTimeout(() => controller.abort(), effectiveTimeout) : null;
353
+
354
+ if (externalSignal) {
355
+ if (externalSignal.aborted) {
356
+ controller.abort(externalSignal.reason);
357
+ } else {
358
+ const onAbort = () => controller.abort(externalSignal.reason);
359
+ externalSignal.addEventListener('abort', onAbort, { once: true });
360
+ return {
361
+ signal: controller.signal,
362
+ cleanup: () => {
363
+ if (timer) clearTimeout(timer);
364
+ externalSignal.removeEventListener('abort', onAbort);
365
+ },
366
+ };
367
+ }
368
+ }
369
+
370
+ return {
371
+ signal: controller.signal,
372
+ cleanup: () => {
373
+ if (timer) clearTimeout(timer);
374
+ },
375
+ };
376
+ }
377
+
378
+ private async handleError(response: Response): Promise<never> {
379
+ const text = await response.text();
380
+ let message: string;
381
+ let code: string;
382
+
383
+ try {
384
+ const json = JSON.parse(text) as { error?: string; code?: string };
385
+ message = json.error ?? text;
386
+ code = json.code ?? 'UNKNOWN';
387
+ } catch {
388
+ message = text || response.statusText;
389
+ code = 'UNKNOWN';
390
+ }
391
+
392
+ if (response.status === 401) {
393
+ throw new SpekoAuthError(message);
394
+ }
395
+
396
+ if (response.status === 429) {
397
+ const retryAfter = response.headers.get('Retry-After');
398
+ throw new SpekoRateLimitError(message, retryAfter ? parseInt(retryAfter, 10) : null);
399
+ }
400
+
401
+ throw new SpekoApiError(message, response.status, code);
402
+ }
403
+ }
404
+
405
+ function drainSseEvents(buffer: string): {
406
+ items: Array<{ event: string; id?: string; data: unknown }>;
407
+ remainder: string;
408
+ } {
409
+ const items: Array<{ event: string; id?: string; data: unknown }> = [];
410
+ let cursor = 0;
411
+
412
+ while (true) {
413
+ const next = buffer.indexOf('\n\n', cursor);
414
+ if (next === -1) break;
415
+ const block = buffer.slice(cursor, next);
416
+ cursor = next + 2;
417
+ if (!block.trim()) continue;
418
+
419
+ let event = 'message';
420
+ let id: string | undefined;
421
+ const dataLines: string[] = [];
422
+ for (const line of block.split(/\r?\n/)) {
423
+ if (line.startsWith('event:')) {
424
+ event = line.slice('event:'.length).trim();
425
+ } else if (line.startsWith('id:')) {
426
+ id = line.slice('id:'.length).trim();
427
+ } else if (line.startsWith('data:')) {
428
+ dataLines.push(line.slice('data:'.length).trimStart());
429
+ }
430
+ }
431
+ const rawData = dataLines.join('\n');
432
+ let data: unknown = rawData;
433
+ try {
434
+ data = JSON.parse(rawData);
435
+ } catch {
436
+ // Keep non-JSON SSE payloads as strings.
437
+ }
438
+ items.push({ event, ...(id ? { id } : {}), data });
439
+ }
440
+
441
+ return { items, remainder: buffer.slice(cursor) };
442
+ }
@@ -0,0 +1,211 @@
1
+ import type { HttpClient } from '../http.js';
2
+ import type {
3
+ AgentCallListPage,
4
+ AgentCallListParams,
5
+ AgentCreateParams,
6
+ AgentRow,
7
+ AgentToolCreateParams,
8
+ AgentToolRow,
9
+ AgentToolUpdateParams,
10
+ AgentUpdateParams,
11
+ ChatTool,
12
+ PhoneNumberRow,
13
+ } from '../types/index.js';
14
+
15
+ /**
16
+ * Per-org agent definitions — the system prompt, voice, intent, and
17
+ * routing constraints used when this agent answers (or places) a call.
18
+ *
19
+ * Every agent created via {@link create} auto-provisions a `Default`
20
+ * knowledge base, so callers can upload documents through
21
+ * {@link Speko.knowledgeBases} without an extra setup step.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const agent = await speko.agents.create({
26
+ * name: 'Support Bot',
27
+ * systemPrompt: 'You are a helpful support agent for Acme.',
28
+ * voice: 'sophia',
29
+ * intent: { language: 'en', optimizeFor: 'latency' },
30
+ * });
31
+ *
32
+ * await speko.agents.attachPhoneNumber(agent.id, num.id);
33
+ * ```
34
+ */
35
+ export class Agents {
36
+ readonly tools: AgentTools;
37
+
38
+ constructor(private readonly http: HttpClient) {
39
+ this.tools = new AgentTools(http);
40
+ }
41
+
42
+ list(): Promise<AgentRow[]> {
43
+ return this.http.get<AgentRow[]>('/v1/agents');
44
+ }
45
+
46
+ create(params: AgentCreateParams): Promise<AgentRow> {
47
+ return this.http.post<AgentRow>('/v1/agents', params);
48
+ }
49
+
50
+ get(agentId: string): Promise<AgentRow> {
51
+ return this.http.get<AgentRow>(`/v1/agents/${encodeURIComponent(agentId)}`);
52
+ }
53
+
54
+ update(agentId: string, params: AgentUpdateParams): Promise<AgentRow> {
55
+ return this.http.patch<AgentRow>(`/v1/agents/${encodeURIComponent(agentId)}`, params);
56
+ }
57
+
58
+ delete(agentId: string): Promise<{ deleted: boolean }> {
59
+ return this.http.delete<{ deleted: boolean }>(`/v1/agents/${encodeURIComponent(agentId)}`);
60
+ }
61
+
62
+ /**
63
+ * Bind a phone number to this agent so inbound calls hydrate the
64
+ * agent's pipeline config from the agent row. Internally calls
65
+ * `PATCH /v1/phone-numbers/:id` with `{ agentId }`.
66
+ */
67
+ attachPhoneNumber(agentId: string, phoneNumberId: string): Promise<PhoneNumberRow> {
68
+ return this.http.patch<PhoneNumberRow>(
69
+ `/v1/phone-numbers/${encodeURIComponent(phoneNumberId)}`,
70
+ { agentId },
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Unlink a phone number from any agent. Inbound calls fall back to
76
+ * the number's `dispatchMetadataTemplate` (or fail if neither is
77
+ * configured). Internally calls `PATCH /v1/phone-numbers/:id` with
78
+ * `{ agentId: null }`.
79
+ */
80
+ detachPhoneNumber(phoneNumberId: string): Promise<PhoneNumberRow> {
81
+ return this.http.patch<PhoneNumberRow>(
82
+ `/v1/phone-numbers/${encodeURIComponent(phoneNumberId)}`,
83
+ { agentId: null },
84
+ );
85
+ }
86
+
87
+ /**
88
+ * List recent calls for an agent. Use `next_cursor` from the returned page
89
+ * as `cursor` to page backward in time.
90
+ */
91
+ listCalls(agentId: string, params: AgentCallListParams = {}): Promise<AgentCallListPage> {
92
+ const query = new URLSearchParams();
93
+ if (params.limit !== undefined) query.set('limit', String(params.limit));
94
+ if (params.cursor) query.set('cursor', params.cursor);
95
+ if (params.since) query.set('since', params.since);
96
+ const suffix = query.toString() ? `?${query}` : '';
97
+ return this.http.get<AgentCallListPage>(
98
+ `/v1/agents/${encodeURIComponent(agentId)}/calls${suffix}`,
99
+ );
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Per-agent tool definitions exposed to the LLM mid-call. Four execution
105
+ * modes: `inline` (caller runs the tool), `webhook` (Speko POSTs to your URL
106
+ * with a Standard-Webhooks signature), `builtin` (Speko-managed tools like
107
+ * `search_knowledge_base`, `transfer_call`, `end_call`), and `integration`
108
+ * (an org-installed Speko app action such as Google Calendar or Slack).
109
+ *
110
+ * Webhook secrets are encrypted server-side at creation; the returned
111
+ * row carries a `secretRef` pointer instead of the plaintext.
112
+ *
113
+ * Every method accepts an optional trailing `AbortSignal` to cancel the
114
+ * in-flight request — useful when a calling framework tears down a session
115
+ * mid-call.
116
+ */
117
+ export class AgentTools {
118
+ constructor(private readonly http: HttpClient) {}
119
+
120
+ /**
121
+ * @param opts.available When true, the server returns only tools the agent
122
+ * can actually run right now — integration tools whose backing installation
123
+ * is disconnected/missing are omitted. Use this on the runtime path so the
124
+ * model is never offered a tool that would fail.
125
+ */
126
+ list(
127
+ agentId: string,
128
+ abortSignal?: AbortSignal,
129
+ opts?: { available?: boolean },
130
+ ): Promise<AgentToolRow[]> {
131
+ const query = opts?.available ? '?available=1' : '';
132
+ return this.http.get<AgentToolRow[]>(
133
+ `/v1/agents/${encodeURIComponent(agentId)}/tools${query}`,
134
+ abortSignal,
135
+ );
136
+ }
137
+
138
+ create(
139
+ agentId: string,
140
+ params: AgentToolCreateParams,
141
+ abortSignal?: AbortSignal,
142
+ ): Promise<AgentToolRow> {
143
+ return this.http.post<AgentToolRow>(
144
+ `/v1/agents/${encodeURIComponent(agentId)}/tools`,
145
+ params,
146
+ abortSignal,
147
+ );
148
+ }
149
+
150
+ get(agentId: string, toolId: string, abortSignal?: AbortSignal): Promise<AgentToolRow> {
151
+ return this.http.get<AgentToolRow>(
152
+ `/v1/agents/${encodeURIComponent(agentId)}/tools/${encodeURIComponent(toolId)}`,
153
+ abortSignal,
154
+ );
155
+ }
156
+
157
+ update(
158
+ agentId: string,
159
+ toolId: string,
160
+ params: AgentToolUpdateParams,
161
+ abortSignal?: AbortSignal,
162
+ ): Promise<AgentToolRow> {
163
+ return this.http.patch<AgentToolRow>(
164
+ `/v1/agents/${encodeURIComponent(agentId)}/tools/${encodeURIComponent(toolId)}`,
165
+ params,
166
+ abortSignal,
167
+ );
168
+ }
169
+
170
+ delete(
171
+ agentId: string,
172
+ toolId: string,
173
+ abortSignal?: AbortSignal,
174
+ ): Promise<{ deleted: boolean }> {
175
+ return this.http.delete<{ deleted: boolean }>(
176
+ `/v1/agents/${encodeURIComponent(agentId)}/tools/${encodeURIComponent(toolId)}`,
177
+ abortSignal,
178
+ );
179
+ }
180
+
181
+ /**
182
+ * Fetch this agent's registered tools and convert them into the
183
+ * {@link ChatTool}[] shape that {@link Speko.complete} accepts. Handles all
184
+ * four source kinds (`inline`, `webhook`, `builtin`, `integration`) — load
185
+ * once and pass the result straight to `speko.complete({ tools })`.
186
+ */
187
+ async listChatTools(
188
+ agentId: string,
189
+ abortSignal?: AbortSignal,
190
+ opts?: { available?: boolean },
191
+ ): Promise<ChatTool[]> {
192
+ const rows = await this.list(agentId, abortSignal, opts);
193
+ return rows.map(toChatTool);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Convert a serialized {@link AgentToolRow} into a {@link ChatTool}. The row's
199
+ * `source` is structurally identical to `ChatToolSource` for every kind, so the
200
+ * mapping is a direct passthrough; `executionMode` is derived from `source.kind`.
201
+ */
202
+ function toChatTool(row: AgentToolRow): ChatTool {
203
+ return {
204
+ name: row.name,
205
+ description: row.description,
206
+ parameters: row.parameters,
207
+ executionMode: row.source.kind,
208
+ source: row.source,
209
+ ...(row.preToolSpeech !== undefined && { preToolSpeech: row.preToolSpeech }),
210
+ };
211
+ }
@@ -0,0 +1,40 @@
1
+ import type { HttpClient } from '../http.js';
2
+ import type {
3
+ CancelScheduledCallbackParams,
4
+ ScheduledCallback,
5
+ ScheduledCallbacksListParams,
6
+ } from '../types/index.js';
7
+
8
+ export class Callbacks {
9
+ constructor(private readonly http: HttpClient) {}
10
+
11
+ list(params: ScheduledCallbacksListParams = {}): Promise<{ callbacks: ScheduledCallback[] }> {
12
+ const query = new URLSearchParams();
13
+ if (params.status) query.set('status', params.status);
14
+ if (params.sourceSessionId) query.set('source_session_id', params.sourceSessionId);
15
+ if (params.limit !== undefined) query.set('limit', String(params.limit));
16
+ const suffix = query.toString() ? `?${query}` : '';
17
+ return this.http.get<{ callbacks: ScheduledCallback[] }>(`/v1/callbacks${suffix}`);
18
+ }
19
+
20
+ get(callbackId: string): Promise<ScheduledCallback> {
21
+ return this.http.get<ScheduledCallback>(`/v1/callbacks/${encodeURIComponent(callbackId)}`);
22
+ }
23
+
24
+ cancel(
25
+ callbackId: string,
26
+ params: CancelScheduledCallbackParams = {},
27
+ ): Promise<ScheduledCallback> {
28
+ return this.http.post<ScheduledCallback>(
29
+ `/v1/callbacks/${encodeURIComponent(callbackId)}/cancel`,
30
+ params,
31
+ );
32
+ }
33
+
34
+ dispatch(callbackId: string): Promise<ScheduledCallback> {
35
+ return this.http.post<ScheduledCallback>(
36
+ `/v1/callbacks/${encodeURIComponent(callbackId)}/dispatch`,
37
+ {},
38
+ );
39
+ }
40
+ }