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/CHANGELOG.md +154 -134
- package/LICENSE +21 -21
- package/README.md +305 -305
- package/dist/blackbox.js +87 -23
- package/dist/index.d.ts +26 -5
- package/dist/index.js +48 -10
- package/package.json +15 -15
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 =
|
|
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 =
|
|
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
|
|
219
|
-
|
|
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) => [
|
|
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
|
|
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
|
-
|
|
256
|
-
|
|
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,
|
|
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 =
|
|
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 && (
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
302
|
-
check("content", true, `${
|
|
303
|
-
await verifyAnchors((bundle.anchors
|
|
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(
|
|
442
|
-
const sealedAt = Math.max(0, ...checkpoints.filter((c) => c && typeof c === "object").map((c) =>
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|