dyadic 1.3.1 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/LICENSE +21 -0
- package/README.md +193 -2
- package/dist/blackbox.d.ts +266 -0
- package/dist/blackbox.js +852 -0
- package/dist/index.d.ts +1095 -11
- package/dist/index.js +560 -29
- package/dist/otel.d.ts +103 -0
- package/dist/otel.js +305 -0
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,95 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.1 (2026-10-17)
|
|
4
|
+
|
|
5
|
+
- Corrected: `anthropic.messages.batches.create` described message batches as running at half price. Dyadic runs them in the
|
|
6
|
+
background at the usual price; nothing about the calls changed.
|
|
7
|
+
|
|
8
|
+
## 1.7.0 (2026-10-17)
|
|
9
|
+
|
|
10
|
+
The Anthropic API and OpenRouter's.
|
|
11
|
+
|
|
12
|
+
- `client.anthropic`: the Anthropic API at `/api/anthropic/v1`, beside the base URL, with the client's key and
|
|
13
|
+
`anthropic-version: 2023-06-01`: `messages.create` (Anthropic's answer; with `stream: true` its events, the stream's
|
|
14
|
+
`text` from the text blocks and `usage` from `message_start` and `message_delta`), `messages.countTokens`,
|
|
15
|
+
`messages.batches.create/list/retrieve/cancel/results/delete` (`results` parses the JSONL), `models.list/retrieve` and
|
|
16
|
+
`files.create/list/retrieve/content/delete`, with types (`AnthropicMessage`, `AnthropicStreamEvent`, `AnthropicBatch`, ...).
|
|
17
|
+
- `client.openrouter`: OpenRouter's API through the AI gateway at `/api/openrouter/v1`: `chat.completions.create`,
|
|
18
|
+
`completions.create`, `embeddings.create`, `models.list/endpoints`, `generation(id)`, `key()` and `credits()` (both
|
|
19
|
+
answered by Dyadic). A key without the gateway throws `DyadicError` with status 403 and code `gateway_not_allowed`:
|
|
20
|
+
Dyadic turns the gateway on for an app's key on request.
|
|
21
|
+
- `Stream<T, U = Usage>`: a stream's usage type is its own (an Anthropic stream's is `AnthropicUsage`); existing code
|
|
22
|
+
is unchanged.
|
|
23
|
+
- Packaging: the package carries its MIT license text (`LICENSE`).
|
|
24
|
+
- Tests: `npm test` runs only the mocked suite (`test/`); `npm run test:production` checks a real API with `DYADIC_LIVE_API_KEY`.
|
|
25
|
+
|
|
26
|
+
## 1.6.0 (2026-10-15)
|
|
27
|
+
|
|
28
|
+
One trace per turn.
|
|
29
|
+
|
|
30
|
+
- `useTrace()`: every following request carries the same W3C `traceparent`, so Dyadic shows a turn's decision, model
|
|
31
|
+
calls and run as one in API Activity and the Black Box; `currentTraceparent()` for requests sent another way. A call's
|
|
32
|
+
own `traceparent` header still wins.
|
|
33
|
+
|
|
34
|
+
## 1.5.0 (2026-10-14)
|
|
35
|
+
|
|
36
|
+
Plans.
|
|
37
|
+
|
|
38
|
+
- `client.plans` (the key's plans switch): `list({ conversation, status, limit, after })`, `create({ title, steps, summary,
|
|
39
|
+
mode, conversation, metadata })`, `retrieve`, `update(id, { revision, ... })` (409 if the plan moved on since),
|
|
40
|
+
`approve(id, { revision })`, `cancel`, `moveStep(id, key, { status, note, verified_by })` and `events(id, { after, limit })`.
|
|
41
|
+
- `client.plans.stream(id, { after, signal })`: the plan live, an async generator of `{ event, id, data }`: `plan.snapshot`
|
|
42
|
+
on each connection, then `plan.created`, `plan.approved`, `plan.step`, ... with the plan as it is then. It connects again
|
|
43
|
+
when the service asks or the connection drops, resuming after the last event (`Last-Event-ID`), stops after `end` (the
|
|
44
|
+
plan completed, cancelled or failed), and honors an AbortSignal.
|
|
45
|
+
- `runs.create({ planMode: true })` investigates read-only and proposes a plan; `runs.create({ planId })` carries out an
|
|
46
|
+
approved plan (refused with `plan_not_runnable` otherwise). Sent as `plan_mode` and `plan_id`, which `RunParams` also
|
|
47
|
+
takes. Streamed runs send `plan.updated` (`RunEvent.plan`); a completed run lists the plans it touched (`Run.plans`).
|
|
48
|
+
- Types: `Plan`, `PlanStep`, `PlanEvent`, `PlanStatus`, `PlanStepStatus`, `PlanStepInput`, `PlanCreateParams`,
|
|
49
|
+
`PlanUpdateParams`, `PlanListQuery`, `PlanStreamEvent`; `BuiltinTool` includes `plans.propose`, `plans.step` and `plans.get`.
|
|
50
|
+
|
|
51
|
+
## 1.4.0 (2026-10-14)
|
|
52
|
+
|
|
53
|
+
Agent Black Box.
|
|
54
|
+
|
|
55
|
+
- `BlackBoxRecorder`: record your own agent's runs (model calls, tool calls, subagents, policy
|
|
56
|
+
decisions, approvals, verified outcomes) into `POST /v1/blackbox/events`, without blocking it:
|
|
57
|
+
bounded queue, batches with an `Idempotency-Key`, per-process sequence numbers so lost events show
|
|
58
|
+
as gaps, metadata only unless `captureContent: true` (secrets redacted first).
|
|
59
|
+
- W3C trace context: `span.traceparent()`, `recorder.start({ traceparent })` for remote subagents.
|
|
60
|
+
- `instrumentMcpClient` (MCP tool calls).
|
|
61
|
+
- `BlackBoxSpanExporter`: an OpenTelemetry `SpanExporter` (SDK 1.x and 2.x, no runtime dependency) that sends
|
|
62
|
+
GenAI and OpenInference spans as OTLP/JSON; content attributes only with `captureContent: true`, values redacted
|
|
63
|
+
first, batches split to fit the service. `spanToOtlp` and `toOtlpRequests` convert without sending.
|
|
64
|
+
- `client.blackbox`: `send`, `sendOtlp`, `runs.list/retrieve/events/graph/verify/export/otlp/simulate`,
|
|
65
|
+
`findings`, `diff`, `keys`, `verify`.
|
|
66
|
+
- `verifyBundle`: check an evidence bundle offline (WebCrypto Ed25519; Node 18+, browsers). It also checks a bundle's
|
|
67
|
+
anchors: the anchored checkpoint is in the bundle's log at or after the run's last checkpoint, the Merkle proof leads to
|
|
68
|
+
the root, and the RFC 3161 token's messageImprint is that root (its genTime is reported). The authority's signature on the
|
|
69
|
+
token is not checked in TypeScript, and the report's warnings say so. `readTimestampToken`, `anchorLeaf`, `foldProof`.
|
|
70
|
+
- `client.blackbox` covers every route of the service: `overview`, `settings`, `runs.checkpoints/content/erase/delete`,
|
|
71
|
+
`runs.events({ types })`, `bookmarks.list/create/delete`, `findings.list/update` (calling `findings(query)` still works),
|
|
72
|
+
`rules.list/preview`, `sinks.list/create/retrieve/update/delete/test/rotate/deliveries/retryDelivery`; `sendOtlp(bytes)`
|
|
73
|
+
sends OTLP protobuf.
|
|
74
|
+
- Live streams: `client.blackbox.runs.stream(id, { after, signal })` and `client.blackbox.watch({ signal })`, async generators
|
|
75
|
+
of `{ event, id, data }` over fetch and ReadableStream that connect again when the service asks or the connection drops
|
|
76
|
+
(resuming after the last event), stop after `end` or `deleted`, and honor an AbortSignal.
|
|
77
|
+
- `verifyWebhook(body, headers, secret, { tolerance })` checks a webhook sink's Standard Webhooks signature (WebCrypto HMAC,
|
|
78
|
+
constant-time, several signatures, 5 minutes by default) and returns the message; `signWebhook`, `WebhookVerificationError`.
|
|
79
|
+
- `RunDeletedError` (a `DyadicError`): a run of the key's that was deleted (HTTP 410, `run_deleted`), with `deletion`.
|
|
80
|
+
- The rest of the OpenAI-compatible API: `conversations` (and `items`), stored chat completions (`chat.completions.list/
|
|
81
|
+
retrieve/update/delete`, `messages.list`), `responses.inputTokens.count` and `responses.compact`, `containers` (and
|
|
82
|
+
`files`), `vectorStores` (`files`, `fileBatches`, `search`) and `skills` (and `versions`).
|
|
83
|
+
- `request()` takes `headers`, a raw `content` body and an AbortSignal (`signal`); query values may be arrays.
|
|
84
|
+
- `BuiltinTool` includes `audio.speak` and `audio.transcribe`; `ResponseParams` has `conversation` and `background`.
|
|
85
|
+
|
|
86
|
+
Fixes:
|
|
87
|
+
|
|
88
|
+
- Leaving a `for await` over a stream early (`break`, `return`, a throw) only released the reader: the HTTP request, and the
|
|
89
|
+
completion it was paying for, kept running. It now cancels the body and aborts the request.
|
|
90
|
+
- Requests say who calls: `User-Agent: dyadic-typescript/<version>`, except in browsers (which don't allow it).
|
|
91
|
+
- The README's connected-apps example compiles (`error.body` is `unknown`).
|
|
92
|
+
|
|
3
93
|
## 1.3.1 (2026-10-10)
|
|
4
94
|
|
|
5
95
|
- The version the SDK reports (`__version__` / `VERSION`, and the User-Agent header) is the package's
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dyadic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -67,7 +67,31 @@ const run = await client.runs.run({
|
|
|
67
67
|
console.log(run.output_text);
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
`client.runs.create({ ..., stream: true })` streams the run's events.
|
|
70
|
+
`client.runs.create({ ..., stream: true })` streams the run's events. Leaving a `for await` over any stream early
|
|
71
|
+
(`break`, `return`, a throw) ends its request too, so nothing keeps running (or billing) behind it.
|
|
72
|
+
|
|
73
|
+
## Plans: propose, approve, carry out
|
|
74
|
+
|
|
75
|
+
With the key's plans switch on, a run can plan before it acts. `planMode: true` investigates read-only and proposes a
|
|
76
|
+
plan of Steps; nothing that changes anything runs. Your user reviews it (edit it with `plans.update`), you approve the
|
|
77
|
+
revision they saw, and a run with `planId` carries it out step by step. The service checks the approval: a plan that isn't
|
|
78
|
+
approved at its current revision doesn't run (`plan_not_runnable`). A run that ends with steps left pauses the plan;
|
|
79
|
+
another run with the same `planId` resumes it.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const proposal = await client.runs.create({ agent: "iris", messages: [{ role: "user", content: "Move our blog to a static site." }],
|
|
83
|
+
planMode: true, conversation_id: "ticket-88" });
|
|
84
|
+
let plan = await client.plans.retrieve(proposal.plans![0].id);
|
|
85
|
+
plan = await client.plans.approve(plan.id, { revision: plan.revision });
|
|
86
|
+
await client.runs.create({ agent: "iris", messages: [{ role: "user", content: "Go ahead." }], planId: plan.id, conversation_id: "ticket-88" });
|
|
87
|
+
|
|
88
|
+
for await (const event of client.plans.stream(plan.id)) console.log(event.event, event.data.step, event.data.status);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`client.plans` also has `list({ conversation, status, limit, after })`, `create({ title, steps, mode })`,
|
|
92
|
+
`update(id, { revision, ... })`, `cancel(id)`, `moveStep(id, key, { status, note, verified_by })` and
|
|
93
|
+
`events(id, { after })`. The stream resumes after the last event (Last-Event-ID), ends once the plan is finished, and
|
|
94
|
+
honors an AbortSignal. A key sees only its own plans.
|
|
71
95
|
|
|
72
96
|
## Decisions: typed answers, fast and cheap
|
|
73
97
|
|
|
@@ -95,6 +119,8 @@ With the person's approval, an application can save into their Dyadic
|
|
|
95
119
|
Workspace, naming them by the Discord account linked to their Dyadic account.
|
|
96
120
|
|
|
97
121
|
```ts
|
|
122
|
+
import { Dyadic, DyadicError } from "dyadic";
|
|
123
|
+
|
|
98
124
|
const status = await client.connections.discord(discordId);
|
|
99
125
|
if (!status.authorized) console.log("Allow this app:", status.authorize_url); // show the person this link
|
|
100
126
|
|
|
@@ -103,7 +129,9 @@ try {
|
|
|
103
129
|
text: "Worth keeping", url: "https://discord.com/channels/1/2/3" });
|
|
104
130
|
console.log(saved.url);
|
|
105
131
|
} catch (error) {
|
|
106
|
-
if (error instanceof DyadicError && error.code === "authorization_required")
|
|
132
|
+
if (error instanceof DyadicError && error.code === "authorization_required") {
|
|
133
|
+
console.log("Allow this app:", (error.body as { authorize_url?: string }).authorize_url);
|
|
134
|
+
}
|
|
107
135
|
}
|
|
108
136
|
```
|
|
109
137
|
|
|
@@ -111,4 +139,167 @@ try {
|
|
|
111
139
|
person can continue unless `as_chat: false`), `image` or `file` (with `files`:
|
|
112
140
|
base64, up to 10 of 8 MB).
|
|
113
141
|
|
|
142
|
+
## Conversations, stored completions, containers, vector stores, skills
|
|
143
|
+
|
|
144
|
+
The rest of the OpenAI-compatible API, named as OpenAI's SDK names it:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const conversation = await client.conversations.create({ metadata: { ticket: "88" } });
|
|
148
|
+
await client.responses.create({ model: "iris", input: "Hello", conversation: conversation.id });
|
|
149
|
+
const items = await client.conversations.items.list(conversation.id, { order: "asc" });
|
|
150
|
+
|
|
151
|
+
const { input_tokens } = await client.responses.inputTokens.count({ model: "iris", input: "How long is this?" }); // free
|
|
152
|
+
const compacted = await client.responses.compact({ model: "iris", previous_response_id: "resp_..." }); // send compacted.output next
|
|
153
|
+
|
|
154
|
+
const stored = await client.chat.completions.list({ metadata: { team: "support" } }); // completions made with store: true
|
|
155
|
+
await client.chat.completions.messages.list(stored.data[0].id);
|
|
156
|
+
|
|
157
|
+
const container = await client.containers.create({ name: "scratch" }); // code interpreter switch
|
|
158
|
+
await client.containers.files.create(container.id, { file: { name: "data.csv", data: csv } });
|
|
159
|
+
|
|
160
|
+
const store = await client.vectorStores.create({ name: "docs", file_ids: [file.id] }); // file search switch; no credits
|
|
161
|
+
const hits = await client.vectorStores.search(store.id, { query: "refund policy" });
|
|
162
|
+
|
|
163
|
+
const skill = await client.skills.create({ files: [{ name: "SKILL.md", data: skillMd }, { name: "scripts/read.py", data: script }] });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Also `conversations.retrieve/update/delete`, `conversations.items.create/retrieve/delete`, `chat.completions.retrieve/update/delete`,
|
|
167
|
+
`containers.files.list/retrieve/delete/content.retrieve`, `vectorStores.files.*` and `vectorStores.fileBatches.*`, and
|
|
168
|
+
`skills.versions.*` (a skill is uploaded as its files, each named by its path in the skill, or as one zip).
|
|
169
|
+
|
|
170
|
+
## The Anthropic API and OpenRouter's, on the same key
|
|
171
|
+
|
|
172
|
+
`client.anthropic` speaks [the Anthropic API](https://dyadic.app/developers/#anthropic) at `/api/anthropic/v1`
|
|
173
|
+
(Claude's request and answer shapes, behaving as Anthropic's do) and `client.openrouter` OpenRouter's API through the
|
|
174
|
+
[AI gateway](https://dyadic.app/developers/#openrouter) at `/api/openrouter/v1`, both with this client's key and beside
|
|
175
|
+
its base URL:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const message = await client.anthropic.messages.create({ model: "iris", max_tokens: 1024, messages: [{ role: "user", content: "Hello" }] });
|
|
179
|
+
console.log(message.content, message.stop_reason, message.usage.output_tokens); // Anthropic's answer
|
|
180
|
+
|
|
181
|
+
const stream = await client.anthropic.messages.create({ model: "iris", max_tokens: 1024, stream: true, messages: [{ role: "user", content: "Tell me a story" }] });
|
|
182
|
+
for await (const event of stream) if (event.type === "content_block_delta" && event.delta?.type === "text_delta") process.stdout.write(event.delta.text);
|
|
183
|
+
console.log(stream.usage); // input and output tokens
|
|
184
|
+
|
|
185
|
+
const batch = await client.anthropic.messages.batches.create({ requests: [{ custom_id: "a", params: { model: "iris", max_tokens: 256, messages: [{ role: "user", content: "Hi" }] } }] });
|
|
186
|
+
const results = await client.anthropic.messages.batches.results(batch.id); // once processing_status is "ended"
|
|
187
|
+
|
|
188
|
+
// OpenRouter's models through the AI gateway (Dyadic turns it on for an app's key on request):
|
|
189
|
+
const reply = await client.openrouter.chat.completions.create({ model: "anthropic/claude-haiku-5.5", messages: [{ role: "user", content: "Hello" }] });
|
|
190
|
+
const { data } = await client.openrouter.key(); // this key's limits, answered by Dyadic
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Also `anthropic.messages.countTokens` (free), `anthropic.messages.batches.list/retrieve/cancel/delete`,
|
|
194
|
+
`anthropic.models.list/retrieve`, `anthropic.files.create/list/retrieve/content/delete`, and `openrouter.completions`,
|
|
195
|
+
`openrouter.embeddings`, `openrouter.models.list/endpoints`, `openrouter.generation` and `openrouter.credits`. The
|
|
196
|
+
gateway is a platform decision: Dyadic turns it on for an app's key on request, the key's owner can turn it off, and a
|
|
197
|
+
key without it gets `403 gateway_not_allowed`. Anthropic's own SDKs and Claude Code work too, with the base URL
|
|
198
|
+
`https://dyadic.app/api/anthropic`.
|
|
199
|
+
|
|
200
|
+
## Agent Black Box: a flight recorder for your own agent
|
|
201
|
+
|
|
202
|
+
Record what your agent does into Dyadic's tamper-evident Black Box (hash-chained,
|
|
203
|
+
signed, verifiable offline). Switch **Black Box: send telemetry** (and **read**) on
|
|
204
|
+
for the key first; both are off by default.
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { BlackBoxRecorder, verifyBundle } from "dyadic";
|
|
208
|
+
|
|
209
|
+
const recorder = new BlackBoxRecorder(client);
|
|
210
|
+
await recorder.run({ title: "Book a room", agent: "planner" }, async (run) => {
|
|
211
|
+
run.declare({ allowedTools: ["book_room"], allowedHosts: ["rooms.example.com"] });
|
|
212
|
+
await run.toolCall("book_room", { effect: true, hosts: ["rooms.example.com"] }, async (tool) => tool.result(await bookRoom("Orion")));
|
|
213
|
+
run.verified("booking", "confirmation ORN-1042 read back from the booking API");
|
|
214
|
+
});
|
|
215
|
+
await recorder.flush();
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Spans nest through their own methods (`run.agentSpan("researcher", {}, async (sub) => sub.toolCall(...))`); a
|
|
219
|
+
remote subagent joins the trace with `recorder.start({ traceparent: span.traceparent() })`. Only metadata is sent
|
|
220
|
+
unless `{ captureContent: true }` (secrets redacted first). `instrumentMcpClient(mcpClient, run)` records MCP tool
|
|
221
|
+
calls. `client.blackbox.runs.export(id)` returns an evidence bundle; `await verifyBundle(bundle, { [keyId]: publicKey })`
|
|
222
|
+
checks it offline with WebCrypto. `client.blackbox.runs.simulate(id, { mode })` never runs anything.
|
|
223
|
+
|
|
224
|
+
### Reading, triaging and managing
|
|
225
|
+
|
|
226
|
+
With the key's **read** switch: `client.blackbox.overview()`, `settings()`, `runs.list/retrieve/events({ types })/graph/verify/
|
|
227
|
+
checkpoints/otlp/export`, `diff(a, b)`, `findings.list({ status, severity, rule })`, `rules.list()` and
|
|
228
|
+
`rules.preview({ rules })` (what detection would raise with other settings; nothing is saved). **Write**: `bookmarks.create(runId,
|
|
229
|
+
{ seq, note })`, `findings.update(id, { status: "dismissed" })`. **Content**: `runs.content(id)`. **Manage**: `runs.erase(id)`,
|
|
230
|
+
`runs.delete(id)` and the sinks below. A run of the key's that was deleted throws `RunDeletedError` (HTTP 410). OTLP protobuf
|
|
231
|
+
goes as it is: `client.blackbox.sendOtlp(bytes)`.
|
|
232
|
+
|
|
233
|
+
### Live
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const controller = new AbortController();
|
|
237
|
+
for await (const { event, id, data } of client.blackbox.runs.stream(runId, { signal: controller.signal })) {
|
|
238
|
+
if (event === "event") console.log(id, data.type); // id is the event's sequence number
|
|
239
|
+
if (event === "end") console.log(data.status, data.outcome); // the stream stops after end (or deleted)
|
|
240
|
+
}
|
|
241
|
+
for await (const { event, data } of client.blackbox.watch()) if (event === "run") console.log(data.id, data.status);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Both connect again by themselves when the service asks or the connection drops (a run's stream resumes after its last
|
|
245
|
+
event), and end when you leave the loop or abort the signal. Node 18+ and browsers (fetch and ReadableStream).
|
|
246
|
+
|
|
247
|
+
### Webhooks and collectors
|
|
248
|
+
|
|
249
|
+
`client.blackbox.sinks.create({ kind: "webhook", url, events: ["finding.created", "run.failed"], min_severity: "high" })` sends
|
|
250
|
+
signed JSON as findings are raised and runs end (keep its `secret`: it is shown once; `sinks.rotate(id)` makes a new one);
|
|
251
|
+
`{ kind: "otlp", url, protocol: "http/protobuf", headers }` exports each finished run to an OpenTelemetry collector. Check every
|
|
252
|
+
webhook before trusting it, with the body exactly as it arrived:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { verifyWebhook, WebhookVerificationError } from "dyadic";
|
|
256
|
+
|
|
257
|
+
export async function POST(request: Request) {
|
|
258
|
+
try {
|
|
259
|
+
const message = await verifyWebhook(await request.text(), request.headers, process.env.BLACKBOX_WEBHOOK_SECRET!);
|
|
260
|
+
// message.type: finding.created, run.completed, run.failed, ...; skip repeats by the webhook-id header
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error instanceof WebhookVerificationError) return new Response("bad signature", { status: 400 });
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
return new Response(null, { status: 204 });
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
(Standard Webhooks: HMAC-SHA256, several signatures during a rotation, a timestamp within 5 minutes by default.)
|
|
270
|
+
|
|
271
|
+
### Anchors
|
|
272
|
+
|
|
273
|
+
When the platform anchors its checkpoint logs (RFC 3161 timestamps over a Merkle root), bundles carry the anchor and
|
|
274
|
+
`verifyBundle` checks it: the anchored checkpoint is in the bundle's log at or after the run's last checkpoint, its Merkle
|
|
275
|
+
proof leads to the root, and the timestamp token stamps that root (its time is in the check's detail). The authority's
|
|
276
|
+
signature on the token is **not** checked here (it needs CMS and certificate work this verifier doesn't do); the report says
|
|
277
|
+
so in its warnings. The Python SDK's `verify_bundle` and the bundle's `verify.py` check it.
|
|
278
|
+
|
|
279
|
+
### OpenTelemetry
|
|
280
|
+
|
|
281
|
+
If your agent framework already makes OpenTelemetry spans, send them as they are. `BlackBoxSpanExporter` is a
|
|
282
|
+
`SpanExporter` for the official SDK (1.x and 2.x; nothing from OpenTelemetry is needed at runtime):
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { BasicTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
286
|
+
import { BlackBoxSpanExporter } from "dyadic";
|
|
287
|
+
|
|
288
|
+
const provider = new BasicTracerProvider({ spanProcessors: [new BatchSpanProcessor(new BlackBoxSpanExporter(client))] });
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Spans following the GenAI semantic conventions (`gen_ai.operation.name`: `chat`, `execute_tool`, `invoke_agent`, ...)
|
|
292
|
+
or OpenInference (`openinference.span.kind`) become model calls, tool calls and agents; others are kept as generic
|
|
293
|
+
spans; one run per trace. Content attributes (`gen_ai.input.messages`, `gen_ai.tool.call.arguments`, `input.value`,
|
|
294
|
+
...) stay in your process unless `{ captureContent: true }`, and every value is redacted first (secrets, by default;
|
|
295
|
+
`redact` replaces it). Large batches are split to fit the service (1,000 spans or 4 MB per request), and a failed
|
|
296
|
+
export is reported to the span processor, never thrown. `toOtlpRequests(spans)` gives the OTLP/JSON without sending it.
|
|
297
|
+
|
|
298
|
+
## Tests
|
|
299
|
+
|
|
300
|
+
`npm test` runs the SDK's own tests against a mocked fetch or a local stand-in server: nothing reaches Dyadic and nothing
|
|
301
|
+
is spent. `npm run test:production` runs the live checks in `live_tests/` against a real API, only with
|
|
302
|
+
`DYADIC_LIVE_API_KEY` set (`DYADIC_LIVE_BASE_URL` for another deployment): free listings and token counts, and two
|
|
303
|
+
messages of at most 16 tokens. Use a short-lived key with a small credit cap.
|
|
304
|
+
|
|
114
305
|
Full guide: https://dyadic.app/developers/
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
export declare const BUNDLE_FORMAT = "dyadic.blackbox.bundle/1";
|
|
2
|
+
export declare const LIMITATIONS: string[];
|
|
3
|
+
/** RFC 8785 canonical JSON: sorted keys (UTF-16 code units), no whitespace, ECMAScript numbers and string escapes. */
|
|
4
|
+
export declare function canonical(value: unknown): string;
|
|
5
|
+
export declare function sha256Hex(data: string | Uint8Array): Promise<string>;
|
|
6
|
+
export declare const genesis: (runId: string) => Promise<string>;
|
|
7
|
+
export declare function commitment(salt: Uint8Array, value: unknown): Promise<string>;
|
|
8
|
+
export declare function ed25519Verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): Promise<boolean>;
|
|
9
|
+
export declare const newTraceId: () => string;
|
|
10
|
+
export declare const newSpanId: () => string;
|
|
11
|
+
/** { traceId, spanId, sampled } from a W3C traceparent header (lowercase hex only), or null. */
|
|
12
|
+
export declare function parseTraceparent(header?: string | null): {
|
|
13
|
+
traceId: string;
|
|
14
|
+
spanId: string;
|
|
15
|
+
sampled: boolean;
|
|
16
|
+
} | null;
|
|
17
|
+
export declare const formatTraceparent: (traceId: string, spanId: string, sampled?: boolean) => string;
|
|
18
|
+
/** Credentials replaced with [REDACTED:<kind>] before anything leaves the process (the service redacts again, and more). */
|
|
19
|
+
export declare function redactSecrets(value: unknown, depth?: number): unknown;
|
|
20
|
+
export interface VerifyReport {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
events: number;
|
|
23
|
+
signed_through: number;
|
|
24
|
+
trusted_keys: boolean;
|
|
25
|
+
checks: {
|
|
26
|
+
name: string;
|
|
27
|
+
ok: boolean;
|
|
28
|
+
detail: string;
|
|
29
|
+
}[];
|
|
30
|
+
errors: string[];
|
|
31
|
+
warnings: string[];
|
|
32
|
+
limitations: string[];
|
|
33
|
+
}
|
|
34
|
+
/** The same checks as the service's and the Python SDK's verifier. `trustedKeys` maps key ids to base64 public keys. */
|
|
35
|
+
export declare function verifyBundle(bundle: any, trustedKeys?: Record<string, string>): Promise<VerifyReport>;
|
|
36
|
+
/** What an RFC 3161 TimeStampToken says it stamps (its TSTInfo's messageImprint) and when (genTime). Unchecked: the
|
|
37
|
+
* authority's CMS signature over it is not verified here. */
|
|
38
|
+
export declare function readTimestampToken(token: Uint8Array): {
|
|
39
|
+
hashAlg: string;
|
|
40
|
+
digest: string;
|
|
41
|
+
genTime: string;
|
|
42
|
+
};
|
|
43
|
+
/** A head's leaf in an anchoring batch (RFC 6962 hashing): sha256(0x00 || canonical({v, tenant, tenant_seq, head})). */
|
|
44
|
+
export declare const anchorLeaf: (tenant: string, tenantSeq: number, head: string) => Promise<Uint8Array>;
|
|
45
|
+
/** The root a leaf and its proof lead to: each step's sibling on its side, nodes sha256(0x01 || left || right). */
|
|
46
|
+
export declare function foldProof(leaf: Uint8Array, proof: {
|
|
47
|
+
side: string;
|
|
48
|
+
hash: string;
|
|
49
|
+
}[]): Promise<Uint8Array>;
|
|
50
|
+
/** The message isn't one the sink signed, or its timestamp is too far from now. */
|
|
51
|
+
export declare class WebhookVerificationError extends Error {
|
|
52
|
+
constructor(message: string);
|
|
53
|
+
}
|
|
54
|
+
type HeaderSource = {
|
|
55
|
+
get(name: string): string | null | undefined;
|
|
56
|
+
} | Record<string, string | string[] | undefined | null> | Iterable<[string, string]>;
|
|
57
|
+
/** The webhook-signature value the service sends for `body` (to test your own receiver). */
|
|
58
|
+
export declare function signWebhook(secret: string, messageId: string, timestamp: number, body: string | Uint8Array | ArrayBuffer): Promise<string>;
|
|
59
|
+
/** A Black Box webhook, parsed, once its signature matches and its timestamp is within `tolerance` seconds (300) of now
|
|
60
|
+
* (`now`, in Unix seconds, for tests); otherwise WebhookVerificationError says why. Pass the body exactly as it arrived (the
|
|
61
|
+
* raw text or bytes, before any JSON parsing) and the request's headers (a Headers object, Node's req.headers, or a plain
|
|
62
|
+
* object: case doesn't matter). Several space-separated signatures (a secret being rotated) are fine. WebCrypto: Node 18+,
|
|
63
|
+
* browsers, Deno, Bun, workers. */
|
|
64
|
+
export declare function verifyWebhook<T = any>(body: string | Uint8Array | ArrayBuffer, headers: HeaderSource, secret: string, options?: {
|
|
65
|
+
tolerance?: number;
|
|
66
|
+
now?: number;
|
|
67
|
+
}): Promise<T>;
|
|
68
|
+
export interface BlackBoxEvent {
|
|
69
|
+
id: string;
|
|
70
|
+
type: string;
|
|
71
|
+
status: string;
|
|
72
|
+
name: string;
|
|
73
|
+
time: string;
|
|
74
|
+
seq: number;
|
|
75
|
+
span_id: string;
|
|
76
|
+
parent_span_id: string;
|
|
77
|
+
agent: string;
|
|
78
|
+
attrs: Record<string, unknown>;
|
|
79
|
+
content?: Record<string, unknown>;
|
|
80
|
+
basis?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface RecorderOptions {
|
|
83
|
+
environment?: "production" | "staging" | "development" | "test";
|
|
84
|
+
captureContent?: boolean;
|
|
85
|
+
redact?: ((value: unknown) => unknown) | null;
|
|
86
|
+
batchSize?: number;
|
|
87
|
+
flushIntervalMs?: number;
|
|
88
|
+
maxQueue?: number;
|
|
89
|
+
maxRetries?: number;
|
|
90
|
+
/** Replaces the HTTP call (tests, a proxy). */
|
|
91
|
+
send?: (batch: Record<string, unknown>, idempotencyKey: string) => Promise<unknown>;
|
|
92
|
+
onError?: (error: unknown, batch: Record<string, unknown>) => void;
|
|
93
|
+
}
|
|
94
|
+
interface Sender {
|
|
95
|
+
blackbox: {
|
|
96
|
+
send: (batch: Record<string, unknown>, options?: {
|
|
97
|
+
idempotencyKey?: string;
|
|
98
|
+
}) => Promise<unknown>;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export declare class BlackBoxRecorder {
|
|
102
|
+
readonly stats: {
|
|
103
|
+
queued: number;
|
|
104
|
+
sent: number;
|
|
105
|
+
dropped: number;
|
|
106
|
+
failedBatches: number;
|
|
107
|
+
};
|
|
108
|
+
readonly options: Required<Omit<RecorderOptions, "send" | "onError">> & Pick<RecorderOptions, "onError">;
|
|
109
|
+
private queue;
|
|
110
|
+
private dropped;
|
|
111
|
+
private timer;
|
|
112
|
+
private sending;
|
|
113
|
+
private readonly send;
|
|
114
|
+
constructor(client: Sender | null, options?: RecorderOptions);
|
|
115
|
+
/** Start a run; with `body`, run it and end it (failed if it throws). */
|
|
116
|
+
start(params: {
|
|
117
|
+
title?: string;
|
|
118
|
+
agent?: string;
|
|
119
|
+
agentName?: string;
|
|
120
|
+
runId?: string;
|
|
121
|
+
traceparent?: string;
|
|
122
|
+
}): BlackBoxRun;
|
|
123
|
+
run<T>(params: {
|
|
124
|
+
title?: string;
|
|
125
|
+
agent?: string;
|
|
126
|
+
agentName?: string;
|
|
127
|
+
runId?: string;
|
|
128
|
+
traceparent?: string;
|
|
129
|
+
}, body: (run: BlackBoxRun) => Promise<T> | T): Promise<T>;
|
|
130
|
+
/** @internal */
|
|
131
|
+
enqueue(run: BlackBoxRun, event: BlackBoxEvent): void;
|
|
132
|
+
/** Send everything queued; resolves when it has been sent (or given up on). */
|
|
133
|
+
flush(): Promise<void>;
|
|
134
|
+
private drain;
|
|
135
|
+
private deliver;
|
|
136
|
+
}
|
|
137
|
+
type SpanKind = "run" | "span" | "model" | "tool" | "agent";
|
|
138
|
+
/** A timed piece of a run. Its methods make its children. */
|
|
139
|
+
export declare class BlackBoxSpan {
|
|
140
|
+
readonly run: BlackBoxRun;
|
|
141
|
+
readonly kind: SpanKind;
|
|
142
|
+
readonly name: string;
|
|
143
|
+
readonly parentId: string;
|
|
144
|
+
readonly agent: string;
|
|
145
|
+
private readonly attrs;
|
|
146
|
+
private readonly content?;
|
|
147
|
+
readonly spanId: string;
|
|
148
|
+
ended: boolean;
|
|
149
|
+
private readonly startedAt;
|
|
150
|
+
constructor(run: BlackBoxRun, kind: SpanKind, name: string, parentId: string, agent: string, attrs?: Record<string, unknown>, content?: Record<string, unknown> | undefined, spanId?: string);
|
|
151
|
+
/** @internal */
|
|
152
|
+
begin(): this;
|
|
153
|
+
protected finish(type: string, status: string, attrs?: Record<string, unknown>, content?: Record<string, unknown>): void;
|
|
154
|
+
end(attrs?: Record<string, unknown>): void;
|
|
155
|
+
fail(error?: unknown, attrs?: Record<string, unknown>): void;
|
|
156
|
+
traceparent(): string;
|
|
157
|
+
private around;
|
|
158
|
+
modelCall<T>(model: string, options?: {
|
|
159
|
+
input?: unknown;
|
|
160
|
+
provider?: string;
|
|
161
|
+
[k: string]: unknown;
|
|
162
|
+
}, body?: (call: ModelCallSpan) => Promise<T> | T): Promise<T | undefined>;
|
|
163
|
+
toolCall<T>(name: string, options?: {
|
|
164
|
+
args?: unknown;
|
|
165
|
+
kind?: string;
|
|
166
|
+
effect?: boolean;
|
|
167
|
+
hosts?: string[];
|
|
168
|
+
callId?: string;
|
|
169
|
+
risk?: string;
|
|
170
|
+
[k: string]: unknown;
|
|
171
|
+
}, body?: (tool: ToolCallSpan) => Promise<T> | T): Promise<T | undefined>;
|
|
172
|
+
agentSpan<T>(agentId: string, options?: {
|
|
173
|
+
name?: string;
|
|
174
|
+
task?: unknown;
|
|
175
|
+
[k: string]: unknown;
|
|
176
|
+
}, body?: (agent: BlackBoxSpan) => Promise<T> | T): Promise<T | undefined>;
|
|
177
|
+
span<T>(name: string, attrs?: Record<string, unknown>, body?: (span: BlackBoxSpan) => Promise<T> | T): Promise<T | undefined>;
|
|
178
|
+
/** A decision your own policy made (allow, deny, require_approval). */
|
|
179
|
+
policy(decision: string, rule: string, options?: {
|
|
180
|
+
tool?: string;
|
|
181
|
+
reason?: string;
|
|
182
|
+
policy?: string;
|
|
183
|
+
version?: string;
|
|
184
|
+
}): void;
|
|
185
|
+
note(text: string, attrs?: Record<string, unknown>): void;
|
|
186
|
+
}
|
|
187
|
+
export declare class ModelCallSpan extends BlackBoxSpan {
|
|
188
|
+
/** What the model returned. Never pass hidden reasoning: it is not recorded. */
|
|
189
|
+
responded(options?: {
|
|
190
|
+
output?: unknown;
|
|
191
|
+
usage?: Record<string, number>;
|
|
192
|
+
toolCalls?: {
|
|
193
|
+
id?: string;
|
|
194
|
+
name?: string;
|
|
195
|
+
}[];
|
|
196
|
+
responseModel?: string;
|
|
197
|
+
responseId?: string;
|
|
198
|
+
finishReason?: string;
|
|
199
|
+
}): void;
|
|
200
|
+
}
|
|
201
|
+
export declare class ToolCallSpan extends BlackBoxSpan {
|
|
202
|
+
executing(): void;
|
|
203
|
+
/** It worked, as the tool reports: a claim, until `run.verified` records evidence. */
|
|
204
|
+
result(value?: unknown, attrs?: Record<string, unknown>): void;
|
|
205
|
+
blocked(rule: string, reason?: string, decision?: "deny" | "require_approval"): void;
|
|
206
|
+
}
|
|
207
|
+
export declare class BlackBoxRun extends BlackBoxSpan {
|
|
208
|
+
readonly recorder: BlackBoxRecorder;
|
|
209
|
+
readonly id: string;
|
|
210
|
+
readonly traceId: string;
|
|
211
|
+
readonly title: string;
|
|
212
|
+
readonly agentName: string;
|
|
213
|
+
private readonly parentHeader;
|
|
214
|
+
private readonly remoteParent;
|
|
215
|
+
private seq;
|
|
216
|
+
constructor(recorder: BlackBoxRecorder, params: {
|
|
217
|
+
title?: string;
|
|
218
|
+
agent?: string;
|
|
219
|
+
agentName?: string;
|
|
220
|
+
runId?: string;
|
|
221
|
+
traceparent?: string;
|
|
222
|
+
});
|
|
223
|
+
descriptor(): Record<string, unknown>;
|
|
224
|
+
/** @internal */
|
|
225
|
+
event(type: string, span: BlackBoxSpan, fields?: {
|
|
226
|
+
status?: string;
|
|
227
|
+
name?: string;
|
|
228
|
+
attrs?: Record<string, unknown>;
|
|
229
|
+
content?: Record<string, unknown>;
|
|
230
|
+
}): BlackBoxEvent;
|
|
231
|
+
/** @internal */
|
|
232
|
+
emit(type: string, span: BlackBoxSpan, fields?: Parameters<BlackBoxRun["event"]>[2]): void;
|
|
233
|
+
/** @internal */
|
|
234
|
+
noteEvent(text: string, attrs: Record<string, unknown>): BlackBoxEvent;
|
|
235
|
+
start(): this;
|
|
236
|
+
end(status?: "completed" | "failed" | "cancelled" | "requires_action" | Record<string, unknown>, options?: {
|
|
237
|
+
error?: unknown;
|
|
238
|
+
}): void;
|
|
239
|
+
/** The task and its boundaries: the Black Box flags tools, hosts or steps beyond them. */
|
|
240
|
+
declare(options?: {
|
|
241
|
+
prompt?: string;
|
|
242
|
+
allowedTools?: string[];
|
|
243
|
+
allowedHosts?: string[];
|
|
244
|
+
maxSteps?: number;
|
|
245
|
+
[k: string]: unknown;
|
|
246
|
+
}): void;
|
|
247
|
+
approval(tool: string, decision: "approved" | "denied" | "expired", options?: {
|
|
248
|
+
approvalId?: string | number;
|
|
249
|
+
by?: string;
|
|
250
|
+
}): void;
|
|
251
|
+
/** You checked an effect independently (read back what was written): evidence, not a claim. */
|
|
252
|
+
verified(kind: string, evidence: string, attrs?: Record<string, unknown>): void;
|
|
253
|
+
unverified(kind: string, reason: string): void;
|
|
254
|
+
}
|
|
255
|
+
/** Every tool an MCP client calls, recorded on `span`: the official TypeScript SDK's Client (callTool({ name, arguments })), or anything shaped like it. */
|
|
256
|
+
export declare function instrumentMcpClient<C extends {
|
|
257
|
+
callTool: (params: {
|
|
258
|
+
name: string;
|
|
259
|
+
arguments?: Record<string, unknown>;
|
|
260
|
+
}, ...rest: any[]) => Promise<any>;
|
|
261
|
+
}>(client: C, span: BlackBoxSpan, options?: {
|
|
262
|
+
server?: string;
|
|
263
|
+
host?: string;
|
|
264
|
+
readOnly?: string[];
|
|
265
|
+
}): C;
|
|
266
|
+
export {};
|