dyadic 1.0.0 → 1.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/README.md CHANGED
@@ -13,4 +13,47 @@ const client = new Dyadic({ apiKey: process.env.DYADIC_API_KEY });
13
13
  console.log(await client.ask("Hello!", { model: "iris" }));
14
14
  ```
15
15
 
16
+ ## Runs: your own agent, Dyadic's tools, your functions
17
+
18
+ ```ts
19
+ const run = await client.runs.run({
20
+ agent: { id: "helpdesk", name: "Helpdesk", instructions: "You answer customers of a small shop." },
21
+ messages: [{ role: "user", content: "Where is order 1042?" }],
22
+ builtin_tools: ["data.web_search"],
23
+ functions: {
24
+ lookup_order: {
25
+ description: "Look up an order by id.",
26
+ parameters: { type: "object", properties: { order_id: { type: "string" } }, required: ["order_id"] },
27
+ handler: async ({ order_id }) => ({ order_id, status: "shipped" }),
28
+ },
29
+ },
30
+ end_user: "customer-17",
31
+ });
32
+ console.log(run.output_text);
33
+ ```
34
+
35
+ `client.runs.create({ ..., stream: true })` streams the run's events.
36
+
37
+ ## Connected apps: saving into a person's Workspace
38
+
39
+ With the person's approval, an application can save into their Dyadic
40
+ Workspace, naming them by the Discord account linked to their Dyadic account.
41
+
42
+ ```ts
43
+ const status = await client.connections.discord(discordId);
44
+ if (!status.authorized) console.log("Allow this app:", status.authorize_url); // show the person this link
45
+
46
+ try {
47
+ const saved = await client.workspace.saves.create({ discord_id: discordId, kind: "message", title: "From #general",
48
+ text: "Worth keeping", url: "https://discord.com/channels/1/2/3" });
49
+ console.log(saved.url);
50
+ } catch (error) {
51
+ if (error instanceof DyadicError && error.code === "authorization_required") console.log("Allow this app:", error.body.authorize_url);
52
+ }
53
+ ```
54
+
55
+ `kind` is `message`, `conversation` (with `messages`, saved as a chat the
56
+ person can continue unless `as_chat: false`), `image` or `file` (with `files`:
57
+ base64, up to 10 of 8 MB).
58
+
16
59
  Full guide: https://dyadic.app/developers/
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "1.0.0";
1
+ export declare const VERSION = "1.1.0";
2
2
  export declare const DEFAULT_BASE_URL = "https://dyadic.app/api/openai/v1";
3
3
  export type Model = "iris" | "iris-think" | (string & {});
4
4
  export type Role = "system" | "developer" | "user" | "assistant" | "tool";
@@ -347,10 +347,120 @@ export interface Batch {
347
347
  expires_at: number | null;
348
348
  metadata: Record<string, string>;
349
349
  }
350
+ /** Dyadic's built-in tools an application's run may use. */
351
+ export type BuiltinTool = "data.web_search" | "data.web_fetch" | "image.generate" | "vision.describe_image";
352
+ /** Your own agent, sent with the run: its name and instructions, optional
353
+ * behaviour overrides (tool_rules, safety, style, citations, research: text to
354
+ * replace the default, or false to turn it off) and model choices. */
355
+ export interface AgentConfig {
356
+ id: string;
357
+ name: string;
358
+ instructions?: string;
359
+ behaviour?: Record<string, string | false>;
360
+ models?: Record<string, string>;
361
+ tools?: Record<string, boolean>;
362
+ }
363
+ export interface RunParams {
364
+ agent: string | AgentConfig;
365
+ messages: ChatMessage[];
366
+ builtin_tools?: BuiltinTool[];
367
+ functions?: Tool[];
368
+ end_user?: string;
369
+ conversation_id?: string;
370
+ max_tokens?: number;
371
+ temperature?: number;
372
+ think?: boolean;
373
+ research?: "auto" | "deep";
374
+ stream?: boolean;
375
+ /** Your own cap on what this run may spend, in USD (for example what your user has left). */
376
+ max_cost_usd?: number;
377
+ /** Ask for up to four suggested follow-up questions (`followups` on the completed run). */
378
+ followups?: boolean;
379
+ }
380
+ export interface Run {
381
+ object: "run";
382
+ type: "run.completed";
383
+ run_id: string;
384
+ status: "completed" | "requires_action";
385
+ agent: string;
386
+ output_text: string;
387
+ function_calls: ToolCall[];
388
+ messages: ChatMessage[];
389
+ images: {
390
+ url: string;
391
+ prompt?: string;
392
+ quality?: string;
393
+ }[];
394
+ sources: {
395
+ title?: string;
396
+ url: string;
397
+ }[];
398
+ tools: string[];
399
+ model: string;
400
+ models: string[];
401
+ usage: Usage;
402
+ credits: number;
403
+ cost_usd: number;
404
+ estimated: boolean;
405
+ model_calls: number;
406
+ operation_id: string;
407
+ seconds: number;
408
+ log_id?: string;
409
+ followups?: string[];
410
+ }
411
+ export interface RunEvent {
412
+ type: string;
413
+ [key: string]: unknown;
414
+ }
415
+ /** A function your application runs: its schema and the handler called with the parsed arguments. */
416
+ export interface FunctionHandler {
417
+ description?: string;
418
+ parameters?: Record<string, unknown>;
419
+ handler: (args: any) => unknown | Promise<unknown>;
420
+ }
421
+ /** Connected apps: whether the person with a linked identity approved this application. */
422
+ export interface Connection {
423
+ linked: boolean;
424
+ authorized: boolean;
425
+ scopes?: string[];
426
+ authorize_url?: string;
427
+ link_url?: string;
428
+ }
429
+ export interface SaveFile {
430
+ name?: string;
431
+ media_type?: string;
432
+ data: string;
433
+ }
434
+ export interface SaveMessage {
435
+ role: "user" | "assistant";
436
+ name?: string;
437
+ text: string;
438
+ url?: string;
439
+ }
440
+ export interface WorkspaceSaveParams {
441
+ discord_id: string | number;
442
+ kind: "message" | "conversation" | "image" | "file";
443
+ title?: string;
444
+ text?: string;
445
+ messages?: SaveMessage[];
446
+ as_chat?: boolean;
447
+ files?: SaveFile[];
448
+ url?: string;
449
+ source?: Record<string, unknown>;
450
+ }
451
+ export interface WorkspaceSave {
452
+ saved: {
453
+ type: "reference" | "chat" | "file";
454
+ id: string;
455
+ }[];
456
+ url: string;
457
+ }
458
+ /** `executes`: the request's work may already be done when it fails (a run), so it is retried only when refused before starting. */
350
459
  export interface RequestOptions {
351
460
  form?: FormData;
352
461
  query?: Record<string, string | number | undefined>;
353
462
  raw?: boolean;
463
+ executes?: boolean;
354
464
  }
355
465
  /** Bytes from the API (speech audio, file downloads). */
356
466
  export declare class Binary {
@@ -397,6 +507,8 @@ export declare class Stream<T> implements AsyncIterable<T> {
397
507
  usage: Usage | undefined;
398
508
  /** Responses streams: the final Response once response.completed arrives. */
399
509
  response: ResponseObject | undefined;
510
+ /** Run streams: the completed run once run.completed arrives. */
511
+ run: Run | undefined;
400
512
  readonly requestId: string;
401
513
  constructor(http: Response, controller: AbortController, onDone?: () => void);
402
514
  [Symbol.asyncIterator](): AsyncIterator<T>;
@@ -415,6 +527,27 @@ export declare class Dyadic {
415
527
  /** Low-level request: JSON (or multipart `options.form`) in; JSON, text, Binary (`options.raw`) or a Stream out,
416
528
  * with retries for busy or failing servers. */
417
529
  request<T>(method: string, path: string, body?: unknown, stream?: boolean, options?: RequestOptions): Promise<T>;
530
+ /** Dyadic's own endpoints sit beside the OpenAI-compatible base: .../api/openai/v1 -> .../api/v1. */
531
+ platformURL(path: string): string;
532
+ /** An agent answers a conversation: one of the account's agents by id ("iris") or your own agent inline,
533
+ * with Dyadic's built-in tools and your own functions. A run that calls your functions ends with status
534
+ * "requires_action": run `function_calls`, append `messages` and your results, and run again (or use `run`). */
535
+ readonly runs: {
536
+ create: {
537
+ (params: RunParams & {
538
+ stream: true;
539
+ }): Promise<Stream<RunEvent>>;
540
+ (params: RunParams & {
541
+ stream?: false;
542
+ }): Promise<Run>;
543
+ };
544
+ /** Run to completion, calling your functions whenever the agent asks. The result's `messages` hold
545
+ * everything the run added, ready to append to your conversation. */
546
+ run: (params: Omit<RunParams, "functions" | "stream"> & {
547
+ functions?: Record<string, FunctionHandler>;
548
+ maxRounds?: number;
549
+ }) => Promise<Run>;
550
+ };
418
551
  readonly models: {
419
552
  list: () => Promise<{
420
553
  object: "list";
@@ -529,6 +662,18 @@ export declare class Dyadic {
529
662
  retrieve: (id: string) => Promise<Batch>;
530
663
  cancel: (id: string) => Promise<Batch>;
531
664
  };
665
+ /** Connected apps. Show the person `authorize_url` when `authorized` is false. */
666
+ readonly connections: {
667
+ discord: (discordId: string | number) => Promise<Connection>;
668
+ };
669
+ /** Save into the Workspace of the person with this Discord account, once they approved this application.
670
+ * Not approved yet throws DyadicError with code "authorization_required" (`error.body.authorize_url`);
671
+ * no Dyadic account with that Discord account: "account_not_linked". */
672
+ readonly workspace: {
673
+ saves: {
674
+ create: ({ discord_id, ...fields }: WorkspaceSaveParams) => Promise<WorkspaceSave>;
675
+ };
676
+ };
532
677
  /** One question, one answer: the reply's text. */
533
678
  ask(prompt: string, options?: Omit<ChatCompletionParams, "messages" | "model" | "stream"> & {
534
679
  model?: Model;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // The official TypeScript SDK for the Dyadic API: Iris
2
2
  // through the OpenAI-compatible endpoints at https://dyadic.app/api/openai/v1.
3
- export const VERSION = "1.0.0";
3
+ export const VERSION = "1.1.0";
4
4
  export const DEFAULT_BASE_URL = "https://dyadic.app/api/openai/v1";
5
5
  /** Bytes from the API (speech audio, file downloads). */
6
6
  export class Binary {
@@ -90,12 +90,18 @@ export class Stream {
90
90
  }
91
91
  if (chunk.usage)
92
92
  this.usage = chunk.usage;
93
+ if (chunk.type === "run.completed") {
94
+ this.run = chunk;
95
+ yield chunk;
96
+ continue;
97
+ }
93
98
  if ((chunk.type === "response.completed" || chunk.type === "response.incomplete") && chunk.response) {
94
99
  this.response = chunk.response;
95
100
  this.usage = chunk.response.usage ?? undefined;
96
101
  }
97
102
  const choice = chunk.choices?.[0];
98
- this.text += chunk.type === "response.output_text.delta" ? chunk.delta ?? "" : choice?.delta?.content ?? choice?.text ?? "";
103
+ this.text += chunk.type === "response.output_text.delta" ? chunk.delta ?? "" : chunk.type === "delta" ? chunk.text ?? ""
104
+ : choice?.delta?.content ?? choice?.text ?? "";
99
105
  yield chunk;
100
106
  }
101
107
  }
@@ -110,9 +116,46 @@ export class Stream {
110
116
  abort() { this.controller.abort(); }
111
117
  }
112
118
  const RETRY = new Set([408, 409, 429, 500, 502, 503, 504]);
119
+ // A run may have executed (and been billed) by the time it fails.
120
+ const RUN_RETRY = new Set([429]);
113
121
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
114
122
  export class Dyadic {
115
123
  constructor(options = {}) {
124
+ /** An agent answers a conversation: one of the account's agents by id ("iris") or your own agent inline,
125
+ * with Dyadic's built-in tools and your own functions. A run that calls your functions ends with status
126
+ * "requires_action": run `function_calls`, append `messages` and your results, and run again (or use `run`). */
127
+ this.runs = {
128
+ create: ((params) => this.request("POST", this.platformURL("/runs"), params, Boolean(params.stream), { executes: true })),
129
+ /** Run to completion, calling your functions whenever the agent asks. The result's `messages` hold
130
+ * everything the run added, ready to append to your conversation. */
131
+ run: async (params) => {
132
+ const { functions = {}, maxRounds = 8, ...rest } = params;
133
+ const schemas = Object.entries(functions).map(([name, f]) => ({ type: "function", function: { name, description: f.description ?? "", parameters: f.parameters ?? { type: "object", properties: {} } } }));
134
+ const conversation = [...rest.messages];
135
+ const added = [];
136
+ for (let round = 0; round < maxRounds; round++) {
137
+ const result = await this.runs.create({ ...rest, messages: conversation, functions: schemas.length ? schemas : undefined, stream: false });
138
+ if (result.status !== "requires_action")
139
+ return { ...result, messages: [...added, { role: "assistant", content: result.output_text }] };
140
+ const step = [...result.messages];
141
+ for (const call of result.function_calls) {
142
+ let args = {};
143
+ try {
144
+ args = JSON.parse(call.function.arguments || "{}");
145
+ }
146
+ catch {
147
+ args = {};
148
+ }
149
+ const fn = functions[call.function.name];
150
+ const output = fn ? await fn.handler(args) : { error: "unknown function" };
151
+ step.push({ role: "tool", tool_call_id: call.id, content: typeof output === "string" ? output : JSON.stringify(output) });
152
+ }
153
+ conversation.push(...step);
154
+ added.push(...step);
155
+ }
156
+ throw new DyadicError(`The run still wanted functions after ${maxRounds} rounds.`);
157
+ },
158
+ };
116
159
  this.models = {
117
160
  list: () => this.request("GET", "/models"),
118
161
  retrieve: (model) => this.request("GET", `/models/${model}`),
@@ -171,6 +214,18 @@ export class Dyadic {
171
214
  retrieve: (id) => this.request("GET", `/batches/${id}`),
172
215
  cancel: (id) => this.request("POST", `/batches/${id}/cancel`),
173
216
  };
217
+ /** Connected apps. Show the person `authorize_url` when `authorized` is false. */
218
+ this.connections = {
219
+ discord: (discordId) => this.request("GET", this.platformURL(`/connections/discord/${discordId}`)),
220
+ };
221
+ /** Save into the Workspace of the person with this Discord account, once they approved this application.
222
+ * Not approved yet throws DyadicError with code "authorization_required" (`error.body.authorize_url`);
223
+ * no Dyadic account with that Discord account: "account_not_linked". */
224
+ this.workspace = {
225
+ saves: {
226
+ create: ({ discord_id, ...fields }) => this.request("POST", this.platformURL("/workspace/saves"), { user: { discord_id: String(discord_id) }, ...fields }, false, { executes: true }),
227
+ },
228
+ };
174
229
  // process.env where it exists (Node, Bun, Deno with compat); browsers pass apiKey.
175
230
  const env = globalThis.process?.env ?? {};
176
231
  this.apiKey = options.apiKey ?? env.DYADIC_API_KEY ?? "";
@@ -189,7 +244,7 @@ export class Dyadic {
189
244
  if (body !== undefined && !options.form)
190
245
  headers["Content-Type"] = "application/json";
191
246
  const query = Object.entries(options.query ?? {}).filter(([, v]) => v !== undefined).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
192
- const url = `${this.baseURL}${path}${query ? `?${query}` : ""}`;
247
+ const url = `${path.startsWith("http") ? path : this.baseURL + path}${query ? `?${query}` : ""}`;
193
248
  const payload = options.form ?? (body === undefined ? undefined : JSON.stringify(body));
194
249
  if (this.appName)
195
250
  headers["X-Title"] = this.appName;
@@ -203,7 +258,7 @@ export class Dyadic {
203
258
  }
204
259
  catch (error) {
205
260
  clearTimeout(timer);
206
- if (attempt < this.maxRetries) {
261
+ if (attempt < this.maxRetries && !options.executes) {
207
262
  await sleep(500 * 2 ** attempt);
208
263
  continue;
209
264
  }
@@ -224,7 +279,7 @@ export class Dyadic {
224
279
  clearTimeout(timer);
225
280
  const data = await response.json().catch(() => ({ error: { message: `HTTP ${response.status}` } }));
226
281
  const error = data?.error ?? {};
227
- if (RETRY.has(response.status) && attempt < this.maxRetries && error.type !== "insufficient_quota") {
282
+ if ((options.executes ? RUN_RETRY : RETRY).has(response.status) && attempt < this.maxRetries && error.type !== "insufficient_quota") {
228
283
  const after = Number(response.headers.get("retry-after"));
229
284
  await sleep(Number.isFinite(after) && after > 0 ? Math.min(after, 30) * 1000 : 500 * 2 ** attempt);
230
285
  continue;
@@ -233,6 +288,8 @@ export class Dyadic {
233
288
  requestId: response.headers.get("x-request-id") ?? "", body: data });
234
289
  }
235
290
  }
291
+ /** Dyadic's own endpoints sit beside the OpenAI-compatible base: .../api/openai/v1 -> .../api/v1. */
292
+ platformURL(path) { return this.baseURL.replace(/\/openai(\/v1)?$/, "/v1") + path; }
236
293
  /** One question, one answer: the reply's text. */
237
294
  async ask(prompt, options = {}) {
238
295
  const { model = "iris", system, ...params } = options;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dyadic",
3
- "version": "1.0.0",
4
- "description": "The official TypeScript and JavaScript SDK for the Dyadic API (Iris, OpenAI-compatible).",
3
+ "version": "1.1.0",
4
+ "description": "The official TypeScript and JavaScript SDK for the Dyadic API: runs with your own agents and functions, plus the OpenAI-compatible API.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",