dyadic 1.8.0 → 1.8.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.
package/dist/blackbox.js CHANGED
@@ -26,8 +26,9 @@ const TELEMETRY = new Set(["note", "span.started", "span.ended", "stream.summary
26
26
  export const LIMITATIONS = [
27
27
  "A hash chain shows recorded events were not altered, removed or reordered afterwards; it cannot show that activity the recorder never observed did not happen.",
28
28
  "Events with basis 'reported' are what a producer outside the platform said happened; the platform did not see them.",
29
- "A signature shows the holder of the signing key issued the checkpoint. Anyone holding the key (or a compromised signing service) could sign a different history; compare key ids with keys published independently.",
29
+ "A signature shows the holder of the signing key issued the checkpoint. Anyone holding the key (or a compromised signing service) could sign a different history; compare key ids with keys published independently. An anchor (an RFC 3161 timestamp from an outside authority) fixes the account's checkpoint log in time; a run's checkpoint is compared with that log when the bundle carries it, so one signed again later shows as a mismatch. A bundle without the log, or a log no anchor covers, can't rule a later re-signing out.",
30
30
  "Events after the last checkpoint are chained but not signed.",
31
+ "Whether the run had events after the bundle's last one is checked against the run's recorded head and the account's log in the bundle; both come from the same server. Ask the platform for the run's current head to be sure.",
31
32
  "Timestamps come from the clock named in each event; clocks can be wrong or skewed.",
32
33
  "Analysis in the bundle (graph, findings) is not covered by any signature and is interpretation, not evidence.",
33
34
  ];
@@ -163,6 +164,18 @@ function canonicalText(text) {
163
164
  return null;
164
165
  }
165
166
  }
167
+ /** A whole number from a bundle field, or `fallback` when it isn't one: a damaged or hostile bundle never crashes a check. */
168
+ function whole(value, fallback = 0) {
169
+ if (typeof value === "boolean")
170
+ return fallback;
171
+ const n = Number(value);
172
+ return Number.isInteger(n) ? n : fallback;
173
+ }
174
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
175
+ function after(later, earlier) {
176
+ const a = Date.parse(String(later ?? "")), b = Date.parse(String(earlier ?? ""));
177
+ return Number.isFinite(a) && Number.isFinite(b) && a > b;
178
+ }
166
179
  /** The same checks as the service's and the Python SDK's verifier. `trustedKeys` maps key ids to base64 public keys. */
167
180
  export async function verifyBundle(bundle, trustedKeys) {
168
181
  const checks = [], errors = [], warnings = [];
@@ -175,11 +188,11 @@ export async function verifyBundle(bundle, trustedKeys) {
175
188
  return report(0, 0);
176
189
  }
177
190
  const runId = String(bundle.run?.id ?? ""), tenant = String(bundle.run?.tenant_ref ?? "");
178
- const events = [...(bundle.events ?? [])].sort((a, b) => Number(a.seq) - Number(b.seq));
191
+ const events = (Array.isArray(bundle.events) ? bundle.events : []).filter(isRecord).sort((a, b) => whole(a.seq) - whole(b.seq));
179
192
  let previous = await genesis(runId), expected = 1, broken = 0;
180
193
  const envelopes = [];
181
194
  for (const item of events) {
182
- const seq = Number(item.seq);
195
+ const seq = whole(item.seq);
183
196
  if (seq !== expected) {
184
197
  check(`event ${expected}`, false, `missing: the chain jumps from ${expected - 1} to ${seq}`);
185
198
  broken++;
@@ -213,16 +226,23 @@ export async function verifyBundle(bundle, trustedKeys) {
213
226
  }
214
227
  if (!broken)
215
228
  check("chain", true, `${envelopes.length} events, unbroken from the genesis value`);
216
- const bundled = {};
217
- for (const key of bundle.keys ?? [])
218
- if (key?.key_id && key?.public_key)
219
- bundled[key.key_id] = key.public_key;
229
+ const bundled = {}, retired = {};
230
+ for (const key of Array.isArray(bundle.keys) ? bundle.keys : []) {
231
+ if (!isRecord(key) || !key.key_id || typeof key.public_key !== "string")
232
+ continue;
233
+ bundled[key.key_id] = key.public_key;
234
+ if (key.retired_at)
235
+ retired[key.key_id] = String(key.retired_at);
236
+ }
220
237
  const keys = trustedKeys ?? bundled;
221
238
  if (!trustedKeys)
222
239
  warnings.push("No keys were given: the bundle's own keys were used, which shows only that it is consistent with itself. Compare its key ids with the keys the platform publishes.");
223
- const hashes = new Map(events.map((e) => [Number(e.seq), e.hash]));
240
+ const hashes = new Map(events.map((e) => [whole(e.seq), e.hash]));
241
+ const checkpoints = (Array.isArray(bundle.checkpoints) ? bundle.checkpoints : []).filter(isRecord);
242
+ // The run's checkpoints whose signature verified: compared with the account's log below.
243
+ const valid = [];
224
244
  let sealed = 0;
225
- for (const cp of bundle.checkpoints ?? []) {
245
+ for (const cp of checkpoints) {
226
246
  const label = `checkpoint ${cp.tenant_seq ?? "?"}`;
227
247
  const payload = canonicalText(cp.payload);
228
248
  if (payload === null) {
@@ -247,20 +267,27 @@ export async function verifyBundle(bundle, trustedKeys) {
247
267
  check(label, false, "its hash doesn't match its payload");
248
268
  continue;
249
269
  }
270
+ const ended = retired[String(cp.key_id)];
271
+ if (ended && after(payload.issued_at, ended)) {
272
+ check(label, false, `signed with key '${cp.key_id}' after that key was retired (${ended})`);
273
+ continue;
274
+ }
250
275
  if (payload.kind === "run" || payload.kind === "erasure") {
251
276
  if (payload.run !== runId) {
252
277
  check(label, false, "it is about another run");
253
278
  continue;
254
279
  }
255
- if (hashes.get(Number(payload.seq)) !== payload.head) {
256
- check(label, false, `it signed a different event ${payload.seq} than the bundle holds`);
280
+ const seq = whole(payload.seq);
281
+ if (hashes.get(seq) !== payload.head) {
282
+ check(label, false, `it signed a different event ${seq} than the bundle holds` + (seq > hashes.size ? ` (the bundle ends at event ${hashes.size}: its tail was cut off)` : ""));
257
283
  continue;
258
284
  }
259
- sealed = Math.max(sealed, Number(payload.seq));
285
+ sealed = Math.max(sealed, seq);
286
+ valid.push({ tenantSeq: whole(payload.tenant_seq), text: String(cp.payload) });
260
287
  }
261
288
  check(label, true, `${payload.kind} checkpoint signed by ${cp.key_id}, covering events 1..${payload.seq}`);
262
289
  }
263
- const chain = [...(bundle.tenant_chain ?? [])].sort((a, b) => Number(a.tenant_seq) - Number(b.tenant_seq));
290
+ const chain = (Array.isArray(bundle.tenant_chain) ? bundle.tenant_chain : []).filter(isRecord).sort((a, b) => whole(a.tenant_seq) - whole(b.tenant_seq));
264
291
  let prior = null, chainOk = chain.length > 0;
265
292
  for (const entry of chain) {
266
293
  const payload = canonicalText(entry.payload);
@@ -271,22 +298,61 @@ export async function verifyBundle(bundle, trustedKeys) {
271
298
  chainOk = false;
272
299
  break;
273
300
  }
274
- if (prior && (Number(payload.tenant_seq) !== prior.seq + 1 || payload.prev !== prior.hash)) {
301
+ if (prior && (whole(payload.tenant_seq) !== prior.seq + 1 || payload.prev !== prior.hash)) {
275
302
  check("tenant chain", false, `the account's checkpoint log breaks before ${payload.tenant_seq}`);
276
303
  chainOk = false;
277
304
  break;
278
305
  }
279
- prior = { seq: Number(payload.tenant_seq), hash: await sha256Hex(entry.payload) };
306
+ prior = { seq: whole(payload.tenant_seq), hash: await sha256Hex(entry.payload) };
280
307
  }
281
308
  if (chainOk)
282
309
  check("tenant chain", true, `${chain.length} of the account's checkpoints link without a gap`);
310
+ // Whole, as far as anything in the bundle says: the run's recorded head, the account's log's entries about this run, and
311
+ // each valid checkpoint against the log's entry of its number (the same checks as the Python verifier's verify_completeness).
312
+ const total = events.length, last = hashes.get(total);
313
+ const head = isRecord(bundle.manifest) && isRecord(bundle.manifest.head) ? bundle.manifest.head : null;
314
+ if (head && head.seq !== undefined && head.seq !== null) {
315
+ const seq = whole(head.seq), digest = String(head.hash ?? "");
316
+ if (seq !== total || digest !== (last ?? "")) {
317
+ check("head", false, `the bundle says the run has ${seq} events ending in ${digest.slice(0, 12)}…; it holds ${total}` + (seq > total ? " (events after its last were cut off, or it was exported before the run ended)" : ""));
318
+ }
319
+ else
320
+ check("head", true, "the run's recorded head is the bundle's last event");
321
+ }
322
+ const bySeq = new Map();
323
+ for (const entry of chain) {
324
+ const payload = canonicalText(entry.payload);
325
+ if (payload === null)
326
+ continue;
327
+ bySeq.set(whole(payload.tenant_seq, -1), entry);
328
+ if (payload.run === runId && (payload.kind === "run" || payload.kind === "erasure")) {
329
+ const seq = whole(payload.seq);
330
+ if (seq > total)
331
+ check("tail", false, `the account's checkpoint log signed event ${seq} of this run; the bundle ends at event ${total}, so the events after it were cut off`);
332
+ else if (hashes.get(seq) !== payload.head)
333
+ check("tail", false, `the account's checkpoint log signed a different event ${seq} than the bundle holds`);
334
+ }
335
+ }
336
+ for (const cp of valid) {
337
+ const entry = bySeq.get(cp.tenantSeq);
338
+ if (!entry)
339
+ continue;
340
+ if ((await sha256Hex(String(entry.payload))) !== (await sha256Hex(cp.text))) {
341
+ check(`checkpoint ${cp.tenantSeq} in the account's log`, false, "the run's checkpoint differs from the account's log at the same number: one of them was signed again later");
342
+ }
343
+ }
344
+ if (!chain.length)
345
+ warnings.push("The account's checkpoint log isn't in the bundle: a checkpoint signed again later couldn't be told from the original, and a cut-off tail only from the run's recorded head.");
346
+ if (!valid.length)
347
+ check("signatures", false, "no valid checkpoint covers this run: nothing in the bundle is signed, so it is consistent with itself and nothing more");
283
348
  const commits = new Map();
284
349
  for (const envelope of envelopes)
285
350
  for (const ref of envelope.content ?? [])
286
351
  if (ref?.ref)
287
352
  commits.set(ref.ref, ref.commit);
288
353
  let bad = 0;
289
- for (const item of bundle.content ?? []) {
354
+ const content = (Array.isArray(bundle.content) ? bundle.content : []).filter(isRecord);
355
+ for (const item of content) {
290
356
  if (!commits.has(item.ref)) {
291
357
  check(`content ${item.ref}`, false, "no event refers to it");
292
358
  bad++;
@@ -298,13 +364,11 @@ export async function verifyBundle(bundle, trustedKeys) {
298
364
  bad++;
299
365
  }
300
366
  }
301
- if ((bundle.content ?? []).length && !bad)
302
- check("content", true, `${bundle.content.length} pieces match their commitments`);
303
- await verifyAnchors((bundle.anchors ?? []).filter((a) => a && typeof a === "object"), chain, bundle.checkpoints ?? [], tenant, check, warnings);
367
+ if (content.length && !bad)
368
+ check("content", true, `${content.length} pieces match their commitments`);
369
+ await verifyAnchors((Array.isArray(bundle.anchors) ? bundle.anchors : []).filter(isRecord), chain, checkpoints, tenant, check, warnings);
304
370
  if (events.length && sealed < events.length)
305
371
  warnings.push(`Events ${sealed + 1}..${events.length} are after the last valid checkpoint: chained, not signed.`);
306
- if (!checks.some((c) => c.name.startsWith("checkpoint") && c.ok))
307
- warnings.push("No valid checkpoint: nothing in the bundle is signed.");
308
372
  return report(events.length, sealed);
309
373
  }
310
374
  // -- anchors: RFC 3161 timestamps over a Merkle root of checkpoint heads ------------------------------------
@@ -438,8 +502,8 @@ async function verifyAnchors(anchors, chain, checkpoints, tenant, check, warning
438
502
  const logged = new Map();
439
503
  for (const entry of chain)
440
504
  if (typeof entry?.payload === "string")
441
- logged.set(Number(entry.tenant_seq ?? 0), await sha256Hex(entry.payload));
442
- const sealedAt = Math.max(0, ...checkpoints.filter((c) => c && typeof c === "object").map((c) => Number(c.tenant_seq ?? 0) || 0));
505
+ logged.set(whole(entry.tenant_seq), await sha256Hex(entry.payload));
506
+ const sealedAt = Math.max(0, ...checkpoints.filter((c) => c && typeof c === "object").map((c) => whole(c.tenant_seq)));
443
507
  for (const anchor of anchors) {
444
508
  const name = `anchor at checkpoint ${anchor.tenant_seq}`;
445
509
  let seq, top, token;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "1.8.0";
1
+ export declare const VERSION = "1.8.2";
2
2
  export declare const DEFAULT_BASE_URL = "https://dyadic.app/api/openai/v1";
3
3
  /** The Anthropic API version the `anthropic` calls speak (sent as anthropic-version, as Anthropic's SDKs do). */
4
4
  export declare const ANTHROPIC_VERSION = "2023-06-01";
@@ -446,6 +446,8 @@ export interface RunParams {
446
446
  /** As the API names them (the same as planMode and planId). */
447
447
  plan_mode?: boolean;
448
448
  plan_id?: string;
449
+ /** How hard the model reasons on each step: low (the default), medium, high or max. */
450
+ effort?: "low" | "medium" | "high" | "max";
449
451
  }
450
452
  export interface Run {
451
453
  object: "run";
@@ -465,6 +467,8 @@ export interface Run {
465
467
  title?: string;
466
468
  url: string;
467
469
  }[];
470
+ /** Speech the run made (audio.generated), when any. */
471
+ audio?: Record<string, unknown>[];
468
472
  tools: string[];
469
473
  model: string;
470
474
  models: string[];
@@ -477,6 +481,13 @@ export interface Run {
477
481
  seconds: number;
478
482
  log_id?: string;
479
483
  followups?: string[];
484
+ /** Where this run is recorded: read it with blackbox.runs.retrieve(blackbox.run_id). The API run_id is the request's. */
485
+ blackbox?: {
486
+ run_id: string;
487
+ trace_id: string;
488
+ };
489
+ /** "content_filter" when the answer was replaced by a refusal; absent otherwise. */
490
+ finish_reason?: "content_filter";
480
491
  /** The plans this run made or moved, as it left them (only when it touched any). */
481
492
  plans?: {
482
493
  id: string;
@@ -520,7 +531,7 @@ export interface PlanEvent {
520
531
  id: number;
521
532
  object: "plan.event";
522
533
  plan_id: string;
523
- kind: "created" | "revised" | "edited" | "approved" | "started" | "resumed" | "step" | "interrupted" | "completed" | "cancelled";
534
+ kind: "created" | "revised" | "edited" | "approved" | "started" | "resumed" | "step" | "interrupted" | "completed" | "cancelled" | "patched";
524
535
  type: string;
525
536
  step: string | null;
526
537
  status: string | null;
@@ -945,11 +956,13 @@ export interface BlackBoxStreamEvent<T = any> {
945
956
  id: string | null;
946
957
  data: T;
947
958
  }
959
+ export declare const STREAM_TRUNCATED = "The stream ended before the answer finished: the connection closed with no final event.";
948
960
  /** A streamed completion: `for await (const chunk of stream)`. `text` and `usage` fill in as it goes. */
949
961
  export declare class Stream<T, U = Usage> implements AsyncIterable<T> {
950
962
  private http;
951
963
  readonly controller: AbortController;
952
964
  private onDone;
965
+ private idleMs;
953
966
  text: string;
954
967
  usage: U | undefined;
955
968
  /** Responses streams: the final Response once response.completed arrives. */
@@ -957,7 +970,8 @@ export declare class Stream<T, U = Usage> implements AsyncIterable<T> {
957
970
  /** Run streams: the completed run once run.completed arrives. */
958
971
  run: Run | undefined;
959
972
  readonly requestId: string;
960
- constructor(http: Response, controller: AbortController, onDone?: () => void);
973
+ /** `idleMs`: abort the stream once nothing arrived for that long (the service sends a keep-alive every 15 s); 0 never. */
974
+ constructor(http: Response, controller: AbortController, onDone?: () => void, idleMs?: number);
961
975
  [Symbol.asyncIterator](): AsyncIterator<T>;
962
976
  /** Read to the end and return the whole text. */
963
977
  finalText(): Promise<string>;
@@ -1547,15 +1561,20 @@ export declare class Dyadic {
1547
1561
  /** OTLP/HTTP traces: an object is sent as OTLP/JSON, bytes as OTLP protobuf (application/x-protobuf; the answer is then
1548
1562
  * the protobuf ExportTraceServiceResponse, as Binary). */
1549
1563
  sendOtlp: (request: Record<string, unknown> | Uint8Array | ArrayBuffer) => Promise<Record<string, unknown>> | Promise<Binary>;
1550
- /** Counts for this key's runs: by status and outcome, open findings, recording gaps. */
1551
- overview: () => Promise<Record<string, unknown>>;
1564
+ /** Counts for this key's runs of the last `days` (7 or 30): by status and outcome, open findings, recording gaps. Each
1565
+ * count is what `runs.list` gives with the same filter and range. */
1566
+ overview: (days?: 7 | 30) => Promise<Record<string, unknown>>;
1552
1567
  /** What the account keeps (recording, capture by kind) and for how long. */
1553
1568
  settings: () => Promise<Record<string, unknown>>;
1554
1569
  runs: {
1570
+ /** Newest first. Filters: q, status, outcome, agent, model, tool, environment, source, since, until, severity, gaps ("1":
1571
+ * runs whose record has a gap), cursor, limit; `next` in the result pages on. */
1555
1572
  list: (query?: Record<string, string | number | undefined>) => Promise<{
1556
1573
  runs: BlackBoxRunSummary[];
1557
1574
  next: string | null;
1558
1575
  }>;
1576
+ /** The values `list` filters take for this key's runs (statuses, outcomes, agents, environments, sources). */
1577
+ options: () => Promise<Record<string, unknown>>;
1559
1578
  retrieve: (id: string) => Promise<BlackBoxRunSummary>;
1560
1579
  /** The run's events in chain order; `types` keeps only those types (up to 20). */
1561
1580
  events: (id: string, query?: {
@@ -1702,6 +1721,8 @@ export declare class Dyadic {
1702
1721
  }[];
1703
1722
  }>;
1704
1723
  verify: (bundle: Record<string, unknown>) => Promise<VerifyReport>;
1724
+ /** The platform API's OpenAPI description (/api/v1: runs, plans, the Black Box); `openapi()` is the OpenAI-compatible API's. */
1725
+ openapi: () => Promise<Record<string, unknown>>;
1705
1726
  /** This key's runs live (server-sent events): `ready` on each connection, then a `run` event (`data`: the run's summary)
1706
1727
  * whenever one starts, changes status or grows. It connects again by itself and never ends on its own: leave the loop
1707
1728
  * or abort `signal`. */
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
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, and beside them the Anthropic API
3
3
  // (/api/anthropic/v1) and OpenRouter's API through the AI gateway (/api/openrouter/v1).
4
- export const VERSION = "1.8.0";
4
+ export const VERSION = "1.8.2";
5
5
  export const DEFAULT_BASE_URL = "https://dyadic.app/api/openai/v1";
6
6
  /** The Anthropic API version the `anthropic` calls speak (sent as anthropic-version, as Anthropic's SDKs do). */
7
7
  export const ANTHROPIC_VERSION = "2023-06-01";
@@ -78,24 +78,44 @@ export class RunDeletedError extends DyadicError {
78
78
  return found && typeof found === "object" ? found : null;
79
79
  }
80
80
  }
81
+ const TERMINAL_EVENTS = new Set(["run.completed", "response.completed", "response.incomplete", "message_stop"]);
82
+ export const STREAM_TRUNCATED = "The stream ended before the answer finished: the connection closed with no final event.";
81
83
  /** A streamed completion: `for await (const chunk of stream)`. `text` and `usage` fill in as it goes. */
82
84
  export class Stream {
83
- constructor(http, controller, onDone = () => { }) {
85
+ /** `idleMs`: abort the stream once nothing arrived for that long (the service sends a keep-alive every 15 s); 0 never. */
86
+ constructor(http, controller, onDone = () => { }, idleMs = 0) {
84
87
  this.http = http;
85
88
  this.controller = controller;
86
89
  this.onDone = onDone;
90
+ this.idleMs = idleMs;
87
91
  this.text = "";
88
92
  this.requestId = http.headers.get("x-request-id") ?? "";
89
93
  }
90
94
  async *[Symbol.asyncIterator]() {
91
95
  const reader = this.http.body.getReader();
92
96
  const decoder = new TextDecoder();
93
- let buffer = "", finished = false;
97
+ let buffer = "", finished = false, whole = false, idle = null;
98
+ const watch = () => { if (!this.idleMs)
99
+ return; clearTimeout(idle); idle = setTimeout(() => this.controller.abort(), this.idleMs); idle?.unref?.(); };
94
100
  try {
101
+ watch();
95
102
  for (;;) {
96
- const { value, done } = await reader.read();
103
+ let value, done;
104
+ try {
105
+ ({ value, done } = await reader.read());
106
+ }
107
+ catch (error) {
108
+ if (error instanceof DyadicError)
109
+ throw error;
110
+ throw new DyadicError(`The stream dropped: ${error?.message ?? error}`, { status: 0 });
111
+ }
112
+ watch();
97
113
  if (done) {
98
114
  finished = true;
115
+ // A stream that closes before its final event ([DONE], run.completed, response.completed/incomplete, message_stop)
116
+ // was cut off: never returned as a whole answer with the text so far.
117
+ if (!whole)
118
+ throw new DyadicError(STREAM_TRUNCATED, { status: 0 });
99
119
  return;
100
120
  }
101
121
  buffer += decoder.decode(value, { stream: true });
@@ -108,9 +128,18 @@ export class Stream {
108
128
  continue;
109
129
  if (data === "[DONE]") {
110
130
  finished = true;
131
+ whole = true;
111
132
  return;
112
133
  }
113
- const chunk = JSON.parse(data);
134
+ let chunk;
135
+ try {
136
+ chunk = JSON.parse(data);
137
+ }
138
+ catch {
139
+ throw new DyadicError(`The stream sent a frame that isn't JSON: ${data.slice(0, 80)}`, { status: 0 });
140
+ }
141
+ if (TERMINAL_EVENTS.has(chunk?.type))
142
+ whole = true;
114
143
  if (chunk.error)
115
144
  throw new DyadicError(chunk.error.message, { status: 200, type: chunk.error.type, code: chunk.error.code, body: chunk });
116
145
  if (chunk.type === "response.failed") {
@@ -142,6 +171,7 @@ export class Stream {
142
171
  }
143
172
  }
144
173
  finally {
174
+ clearTimeout(idle);
145
175
  // Leaving early (break, return, a throw) must end the request too: releasing the reader alone leaves the
146
176
  // response, and the completion it is paying for, running.
147
177
  if (!finished) {
@@ -560,12 +590,17 @@ export class Dyadic {
560
590
  ? this.request("POST", this.platformURL("/blackbox/otlp/v1/traces"), undefined, false, { idempotent: true, content: request,
561
591
  headers: { "Content-Type": "application/x-protobuf", Accept: "application/x-protobuf" } })
562
592
  : this.request("POST", this.platformURL("/blackbox/otlp/v1/traces"), request, false, { idempotent: true }),
563
- /** Counts for this key's runs: by status and outcome, open findings, recording gaps. */
564
- overview: () => this.request("GET", this.platformURL("/blackbox/overview")),
593
+ /** Counts for this key's runs of the last `days` (7 or 30): by status and outcome, open findings, recording gaps. Each
594
+ * count is what `runs.list` gives with the same filter and range. */
595
+ overview: (days = 7) => this.request("GET", this.platformURL("/blackbox/overview"), undefined, false, { query: { days } }),
565
596
  /** What the account keeps (recording, capture by kind) and for how long. */
566
597
  settings: () => this.request("GET", this.platformURL("/blackbox/settings")),
567
598
  runs: {
599
+ /** Newest first. Filters: q, status, outcome, agent, model, tool, environment, source, since, until, severity, gaps ("1":
600
+ * runs whose record has a gap), cursor, limit; `next` in the result pages on. */
568
601
  list: (query = {}) => this.request("GET", this.platformURL("/blackbox/runs"), undefined, false, { query }),
602
+ /** The values `list` filters take for this key's runs (statuses, outcomes, agents, environments, sources). */
603
+ options: () => this.request("GET", this.platformURL("/blackbox/runs/options")),
569
604
  retrieve: (id) => this.request("GET", this.platformURL(`/blackbox/runs/${id}`)),
570
605
  /** The run's events in chain order; `types` keeps only those types (up to 20). */
571
606
  events: (id, query = {}) => this.request("GET", this.platformURL(`/blackbox/runs/${id}/events`), undefined, false, { query: { after: query.after, limit: query.limit, types: Array.isArray(query.types) ? query.types.join(",") || undefined : query.types } }),
@@ -628,6 +663,8 @@ export class Dyadic {
628
663
  diff: (a, b) => this.request("GET", this.platformURL("/blackbox/diff"), undefined, false, { query: { a, b } }),
629
664
  keys: () => this.request("GET", this.platformURL("/blackbox/keys")),
630
665
  verify: (bundle) => this.request("POST", this.platformURL("/blackbox/verify"), bundle, false, { idempotent: true }),
666
+ /** The platform API's OpenAPI description (/api/v1: runs, plans, the Black Box); `openapi()` is the OpenAI-compatible API's. */
667
+ openapi: () => this.request("GET", this.platformURL("/openapi.json")),
631
668
  /** This key's runs live (server-sent events): `ready` on each connection, then a `run` event (`data`: the run's summary)
632
669
  * whenever one starts, changes status or grows. It connects again by itself and never ends on its own: leave the loop
633
670
  * or abort `signal`. */
@@ -708,8 +745,10 @@ export class Dyadic {
708
745
  * are retried on busy or failing servers; anything else only if it provably never started, unless `options.idempotent`. */
709
746
  async request(method, path, body, stream = false, options = {}) {
710
747
  const { response, controller, release } = await this.open(method, path, body, stream, options);
748
+ // A stream outlives a request's deadline (its headers came within it); from then on it is held to silence: the
749
+ // service's keep-alives (every 15 s) show it is alive. Before, a streamed run was aborted at timeoutMs whatever it did.
711
750
  if (stream)
712
- return new Stream(response, controller, release);
751
+ return new Stream(response, controller, release, this.timeoutMs);
713
752
  try {
714
753
  const kind = (response.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
715
754
  if (!options.raw && (kind === "application/json" || kind.endsWith("+json")))
@@ -777,8 +816,7 @@ export class Dyadic {
777
816
  release();
778
817
  throw new DyadicError(`The API answered with a redirect (HTTP ${response.status}); not following it with your key.`, { status: response.status });
779
818
  }
780
- if (!stream)
781
- clearTimeout(timer);
819
+ clearTimeout(timer); // the headers arrived within the deadline; a stream's silence is watched by Stream itself
782
820
  if (response.ok)
783
821
  return { response, controller, release };
784
822
  release();
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
- {
2
- "name": "dyadic",
3
- "version": "1.8.0",
4
- "description": "The official TypeScript and JavaScript SDK for the Dyadic API: runs with your own agents and functions, plus the OpenAI-, Anthropic- and OpenRouter-compatible APIs.",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
9
- "files": ["dist", "README.md", "CHANGELOG.md", "LICENSE"],
10
- "engines": { "node": ">=18" },
11
- "license": "MIT",
12
- "homepage": "https://dyadic.app/developers/",
13
- "scripts": { "build": "tsc", "pretest": "tsc", "test": "node --test test/client.test.js test/transport.test.js test/blackbox.test.js test/otel.test.js test/api.test.js test/live.test.js test/plans.test.js test/compatible.test.js", "test:production": "tsc && node --test live_tests/production.test.js", "prepack": "tsc" },
14
- "devDependencies": { "@opentelemetry/api": "1.9.1", "@opentelemetry/sdk-trace-base": "2.12.0", "typescript": "5.6.3" }
15
- }
1
+ {
2
+ "name": "dyadic",
3
+ "version": "1.8.2",
4
+ "description": "The official TypeScript and JavaScript SDK for the Dyadic API: runs with your own agents and functions, plus the OpenAI-, Anthropic- and OpenRouter-compatible APIs.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
9
+ "files": ["dist", "README.md", "CHANGELOG.md", "LICENSE"],
10
+ "engines": { "node": ">=18" },
11
+ "license": "MIT",
12
+ "homepage": "https://dyadic.app/developers/",
13
+ "scripts": { "build": "tsc", "pretest": "tsc", "test": "node --test test/client.test.js test/transport.test.js test/blackbox.test.js test/otel.test.js test/api.test.js test/live.test.js test/plans.test.js test/compatible.test.js", "test:production": "tsc && node --test live_tests/production.test.js", "prepack": "tsc" },
14
+ "devDependencies": { "@opentelemetry/api": "1.9.1", "@opentelemetry/sdk-trace-base": "2.12.0", "typescript": "5.6.3" }
15
+ }