@openwop/openwop 1.1.1 → 1.1.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/README.md +4 -0
- package/dist/client.d.ts +80 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +186 -0
- package/dist/client.js.map +1 -1
- package/dist/cost-attribution.d.ts +49 -0
- package/dist/cost-attribution.d.ts.map +1 -0
- package/dist/cost-attribution.js +65 -0
- package/dist/cost-attribution.js.map +1 -0
- package/dist/event-helpers.d.ts +95 -0
- package/dist/event-helpers.d.ts.map +1 -0
- package/dist/event-helpers.js +160 -0
- package/dist/event-helpers.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/registry-helpers.d.ts +118 -0
- package/dist/registry-helpers.d.ts.map +1 -0
- package/dist/registry-helpers.js +82 -0
- package/dist/registry-helpers.js.map +1 -0
- package/dist/types.d.ts +376 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/webhook-helpers.d.ts +73 -0
- package/dist/webhook-helpers.d.ts.map +1 -0
- package/dist/webhook-helpers.js +97 -0
- package/dist/webhook-helpers.js.map +1 -0
- package/package.json +1 -1
- package/src/client.ts +218 -0
- package/src/cost-attribution.ts +72 -0
- package/src/event-helpers.ts +238 -0
- package/src/index.ts +96 -0
- package/src/registry-helpers.ts +173 -0
- package/src/types.ts +424 -0
- package/src/webhook-helpers.ts +131 -0
package/src/client.ts
CHANGED
|
@@ -22,11 +22,25 @@ import {
|
|
|
22
22
|
type ErrorEnvelope,
|
|
23
23
|
type ForkRunRequest,
|
|
24
24
|
type ForkRunResponse,
|
|
25
|
+
type GetPromptRequest,
|
|
25
26
|
type InterruptByTokenInspection,
|
|
27
|
+
type DebugBundle,
|
|
28
|
+
type DebugBundleOptions,
|
|
29
|
+
type ListPromptsRequest,
|
|
30
|
+
type ListPromptsResponse,
|
|
31
|
+
type PromptTemplate,
|
|
32
|
+
type RegisterWebhookRequest,
|
|
33
|
+
type RegisterWebhookResponse,
|
|
34
|
+
type PauseRunRequest,
|
|
35
|
+
type PauseRunResponse,
|
|
26
36
|
type PollEventsResponse,
|
|
37
|
+
type RenderPromptRequest,
|
|
38
|
+
type RenderPromptResponse,
|
|
27
39
|
type ResolveInterruptByTokenResponse,
|
|
28
40
|
type ResolveInterruptRequest,
|
|
29
41
|
type ResolveInterruptResponse,
|
|
42
|
+
type ResumeRunRequest,
|
|
43
|
+
type ResumeRunResponse,
|
|
30
44
|
type RunEventDoc,
|
|
31
45
|
type RunSnapshot,
|
|
32
46
|
} from './types.js';
|
|
@@ -106,6 +120,33 @@ export class OpenwopClient {
|
|
|
106
120
|
path: `/v1/runs/${encodeURIComponent(runId)}`,
|
|
107
121
|
}),
|
|
108
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Fetch the portable JSON diagnostic export for a single run per
|
|
125
|
+
* `spec/v1/debug-bundle.md`. The bundle's `redactionMode` reflects
|
|
126
|
+
* the host's advertised `capabilities.compliance.defaultMode`; the
|
|
127
|
+
* caller MUST treat masked/omitted/hashed fields as the
|
|
128
|
+
* spec-canonical value. The `truncated` + `truncatedReason` fields
|
|
129
|
+
* indicate the host hit its size cap.
|
|
130
|
+
*
|
|
131
|
+
* Returns `null` when the host doesn't advertise
|
|
132
|
+
* `capabilities.debugBundle.supported: true` (the endpoint returns
|
|
133
|
+
* 404 in that case per `debug-bundle.md` §"Authorization").
|
|
134
|
+
*/
|
|
135
|
+
debugBundle: async (runId: string, opts: DebugBundleOptions = {}): Promise<DebugBundle | null> => {
|
|
136
|
+
const params = new URLSearchParams();
|
|
137
|
+
if (opts.maxEvents !== undefined) params.set('maxEvents', String(opts.maxEvents));
|
|
138
|
+
const query = params.toString();
|
|
139
|
+
const path = `/v1/runs/${encodeURIComponent(runId)}/debug-bundle${query ? `?${query}` : ''}`;
|
|
140
|
+
try {
|
|
141
|
+
return await this.#request<DebugBundle>({ method: 'GET', path });
|
|
142
|
+
} catch (err) {
|
|
143
|
+
// Host doesn't advertise the capability → 404. Surface as null so callers
|
|
144
|
+
// can branch on capability discovery without try/catch.
|
|
145
|
+
if (err instanceof Error && /\b404\b/.test(err.message)) return null;
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
109
150
|
cancel: (
|
|
110
151
|
runId: string,
|
|
111
152
|
body: CancelRunRequest = {},
|
|
@@ -118,6 +159,30 @@ export class OpenwopClient {
|
|
|
118
159
|
headers: this.#mutationHeaders(opts),
|
|
119
160
|
}),
|
|
120
161
|
|
|
162
|
+
pause: (
|
|
163
|
+
runId: string,
|
|
164
|
+
body: PauseRunRequest = {},
|
|
165
|
+
opts: MutationOptions = {},
|
|
166
|
+
): Promise<PauseRunResponse> =>
|
|
167
|
+
this.#request<PauseRunResponse>({
|
|
168
|
+
method: 'POST',
|
|
169
|
+
path: `/v1/runs/${encodeURIComponent(runId)}:pause`,
|
|
170
|
+
body,
|
|
171
|
+
headers: this.#mutationHeaders(opts),
|
|
172
|
+
}),
|
|
173
|
+
|
|
174
|
+
resume: (
|
|
175
|
+
runId: string,
|
|
176
|
+
body: ResumeRunRequest = {},
|
|
177
|
+
opts: MutationOptions = {},
|
|
178
|
+
): Promise<ResumeRunResponse> =>
|
|
179
|
+
this.#request<ResumeRunResponse>({
|
|
180
|
+
method: 'POST',
|
|
181
|
+
path: `/v1/runs/${encodeURIComponent(runId)}:resume`,
|
|
182
|
+
body,
|
|
183
|
+
headers: this.#mutationHeaders(opts),
|
|
184
|
+
}),
|
|
185
|
+
|
|
121
186
|
/**
|
|
122
187
|
* Bulk-cancel a set of in-flight runs in a single request per
|
|
123
188
|
* `rest-endpoints.md` §"POST /v1/runs:bulk-cancel" (closes R1).
|
|
@@ -229,6 +294,159 @@ export class OpenwopClient {
|
|
|
229
294
|
),
|
|
230
295
|
};
|
|
231
296
|
|
|
297
|
+
// ── Webhook subscriptions (per spec/v1/webhooks.md) ─────────────────────
|
|
298
|
+
readonly webhooks = {
|
|
299
|
+
/**
|
|
300
|
+
* Register a webhook subscription. Server signs deliveries with
|
|
301
|
+
* HMAC-SHA256 over `${timestamp}.${rawBody}` using the
|
|
302
|
+
* registration-time secret per `spec/v1/webhooks.md` §"Signature
|
|
303
|
+
* recipe". The secret is returned ONCE in the response — store it
|
|
304
|
+
* server-side for verification; the host cannot recover it.
|
|
305
|
+
*/
|
|
306
|
+
register: (
|
|
307
|
+
body: RegisterWebhookRequest,
|
|
308
|
+
opts: MutationOptions = {},
|
|
309
|
+
): Promise<RegisterWebhookResponse> =>
|
|
310
|
+
this.#request<RegisterWebhookResponse>({
|
|
311
|
+
method: 'POST',
|
|
312
|
+
path: '/v1/webhooks',
|
|
313
|
+
body,
|
|
314
|
+
headers: this.#mutationHeaders(opts),
|
|
315
|
+
}),
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Unregister a webhook subscription. Returns void on success;
|
|
319
|
+
* throws `WopError` with `subscription_not_found` on unknown
|
|
320
|
+
* subscriptionId.
|
|
321
|
+
*/
|
|
322
|
+
unregister: async (subscriptionId: string): Promise<void> => {
|
|
323
|
+
await this.#request<unknown>({
|
|
324
|
+
method: 'DELETE',
|
|
325
|
+
path: `/v1/webhooks/${encodeURIComponent(subscriptionId)}`,
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// ── Prompt library (RFC 0028; gated on capabilities.prompts.*) ──
|
|
331
|
+
//
|
|
332
|
+
// Read endpoints (list, get, render) gate on
|
|
333
|
+
// `capabilities.prompts.endpointsSupported: true`. Mutating endpoints
|
|
334
|
+
// (create, update, delete) additionally require
|
|
335
|
+
// `capabilities.prompts.mutableLibrary: true`. Hosts that don't advertise
|
|
336
|
+
// the relevant capability return `501 capability_not_provided`; the SDK
|
|
337
|
+
// surfaces that as a `WopError`. Clients SHOULD pre-flight via
|
|
338
|
+
// `getCapabilities()` before calling.
|
|
339
|
+
//
|
|
340
|
+
// NOTE: `capabilities.prompts.supported: true` (without
|
|
341
|
+
// `endpointsSupported: true`) ONLY gates node-execution PromptRef
|
|
342
|
+
// resolution per RFC 0027 Phase A; it does NOT imply these endpoints are
|
|
343
|
+
// available. See spec/v1/prompts.md §"Capability advertisement" for the
|
|
344
|
+
// two-axis gating split.
|
|
345
|
+
readonly prompts = {
|
|
346
|
+
/**
|
|
347
|
+
* List prompt templates available to the caller per RFC 0028 §A
|
|
348
|
+
* (operationId `listPromptTemplates`). Supports kind / tag / modelClass
|
|
349
|
+
* / source filters + opaque cursor pagination.
|
|
350
|
+
*/
|
|
351
|
+
list: (req: ListPromptsRequest = {}): Promise<ListPromptsResponse> => {
|
|
352
|
+
const search = new URLSearchParams();
|
|
353
|
+
if (req.kind) search.set('kind', req.kind);
|
|
354
|
+
if (req.tag) search.set('tag', req.tag);
|
|
355
|
+
if (req.modelClass) search.set('modelClass', req.modelClass);
|
|
356
|
+
if (req.source) search.set('source', req.source);
|
|
357
|
+
if (req.cursor) search.set('cursor', req.cursor);
|
|
358
|
+
if (req.limit !== undefined) search.set('limit', String(req.limit));
|
|
359
|
+
const query = search.toString();
|
|
360
|
+
return this.#request<ListPromptsResponse>({
|
|
361
|
+
method: 'GET',
|
|
362
|
+
path: `/v1/prompts${query ? `?${query}` : ''}`,
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Fetch a single PromptTemplate by id per RFC 0028 §A
|
|
368
|
+
* (operationId `getPromptTemplate`). Optionally pin a SemVer
|
|
369
|
+
* `version`; supply `libraryId` to disambiguate when multiple installed
|
|
370
|
+
* packs ship the same templateId.
|
|
371
|
+
*/
|
|
372
|
+
get: (req: GetPromptRequest): Promise<PromptTemplate> => {
|
|
373
|
+
const search = new URLSearchParams();
|
|
374
|
+
if (req.version) search.set('version', req.version);
|
|
375
|
+
if (req.libraryId) search.set('libraryId', req.libraryId);
|
|
376
|
+
const query = search.toString();
|
|
377
|
+
return this.#request<PromptTemplate>({
|
|
378
|
+
method: 'GET',
|
|
379
|
+
path: `/v1/prompts/${encodeURIComponent(req.templateId)}${query ? `?${query}` : ''}`,
|
|
380
|
+
});
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Render a PromptTemplate with supplied variable bindings per RFC 0028
|
|
385
|
+
* §A (operationId `renderPromptTemplate`). Returns composed body +
|
|
386
|
+
* sha256 hash + per-variable hashes. The deterministic-hash invariant
|
|
387
|
+
* (RFC 0028 §A) requires the `hash` to match what a matching
|
|
388
|
+
* `prompt.composed` event would carry at dispatch time. Does NOT
|
|
389
|
+
* dispatch an LLM call. Secret-source variable values MUST be supplied
|
|
390
|
+
* as `[REDACTED:<credentialRef>]` markers per
|
|
391
|
+
* SECURITY/threat-model-secret-leakage.md §SR-1.
|
|
392
|
+
*/
|
|
393
|
+
render: (req: RenderPromptRequest): Promise<RenderPromptResponse> => {
|
|
394
|
+
return this.#request<RenderPromptResponse>({
|
|
395
|
+
method: 'POST',
|
|
396
|
+
path: '/v1/prompts:render',
|
|
397
|
+
body: req,
|
|
398
|
+
});
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Create a new user-source PromptTemplate per RFC 0028 §A
|
|
403
|
+
* (operationId `createPromptTemplate`). Mutating endpoint —
|
|
404
|
+
* requires `capabilities.prompts.mutableLibrary: true`. Supports
|
|
405
|
+
* `Idempotency-Key` per the standard `MutationOptions` pattern.
|
|
406
|
+
*/
|
|
407
|
+
create: (template: PromptTemplate, opts: MutationOptions = {}): Promise<void> => {
|
|
408
|
+
return this.#request<void>({
|
|
409
|
+
method: 'POST',
|
|
410
|
+
path: '/v1/prompts',
|
|
411
|
+
body: template,
|
|
412
|
+
headers: this.#mutationHeaders(opts),
|
|
413
|
+
});
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Replace an existing user-source PromptTemplate per RFC 0028 §A
|
|
418
|
+
* (operationId `updatePromptTemplate`). Submitted SemVer MUST be
|
|
419
|
+
* strictly greater than stored. Mutating endpoint — requires
|
|
420
|
+
* `capabilities.prompts.mutableLibrary: true`. Pack-sourced and
|
|
421
|
+
* host-built-in templates are read-only (host returns 403).
|
|
422
|
+
*/
|
|
423
|
+
update: (
|
|
424
|
+
templateId: string,
|
|
425
|
+
template: PromptTemplate,
|
|
426
|
+
opts: MutationOptions = {},
|
|
427
|
+
): Promise<PromptTemplate> => {
|
|
428
|
+
return this.#request<PromptTemplate>({
|
|
429
|
+
method: 'PUT',
|
|
430
|
+
path: `/v1/prompts/${encodeURIComponent(templateId)}`,
|
|
431
|
+
body: template,
|
|
432
|
+
headers: this.#mutationHeaders(opts),
|
|
433
|
+
});
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Delete a user-source PromptTemplate per RFC 0028 §A
|
|
438
|
+
* (operationId `deletePromptTemplate`). Mutating endpoint —
|
|
439
|
+
* requires `capabilities.prompts.mutableLibrary: true`. Pack-sourced
|
|
440
|
+
* and host-built-in templates are read-only (host returns 403).
|
|
441
|
+
*/
|
|
442
|
+
delete: (templateId: string): Promise<void> => {
|
|
443
|
+
return this.#request<void>({
|
|
444
|
+
method: 'DELETE',
|
|
445
|
+
path: `/v1/prompts/${encodeURIComponent(templateId)}`,
|
|
446
|
+
});
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
|
|
232
450
|
// ── Audit-log integrity (gated on openwop-audit-log-integrity profile) ──
|
|
233
451
|
readonly audit = {
|
|
234
452
|
/**
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost-attribution allowlist + sanitizer helpers
|
|
3
|
+
* (`spec/v1/observability.md §"Cost attribution attributes"`).
|
|
4
|
+
*
|
|
5
|
+
* The spec MUSTs that hosts emitting `openwop.cost.*` OTel span attrs
|
|
6
|
+
* route them through an allowlist sanitizer that drops any attribute
|
|
7
|
+
* name outside the canonical set AND any non-primitive value. The
|
|
8
|
+
* `cost-attribution-allowlist-redaction` SECURITY invariant + the
|
|
9
|
+
* `cost-attribution.test.ts` conformance scenario are the public test
|
|
10
|
+
* surface; this module is the SDK-side helper that independent hosts
|
|
11
|
+
* (TypeScript / Python / Go) can share so the allowlist has a single
|
|
12
|
+
* source of truth instead of being re-derived in each runtime.
|
|
13
|
+
*
|
|
14
|
+
* Why this lives in the SDK and not just the conformance suite:
|
|
15
|
+
* implementations need the allowlist at HOST EMIT TIME (before spans
|
|
16
|
+
* are written). Importing from `@openwop/openwop` keeps the runtime
|
|
17
|
+
* + the conformance assertion in lockstep — if a future RFC adds an
|
|
18
|
+
* eighth attribute, one PR updates the constant and both surfaces
|
|
19
|
+
* pick it up.
|
|
20
|
+
*
|
|
21
|
+
* @see spec/v1/observability.md §"Cost attribution attributes"
|
|
22
|
+
* @see SECURITY/invariants.yaml row `cost-attribution-allowlist-redaction`
|
|
23
|
+
* @see conformance/src/scenarios/cost-attribution.test.ts
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Canonical allowlist of cost-attribute names. Mutating this list is
|
|
27
|
+
* a wire-shape change — needs an RFC. */
|
|
28
|
+
export const OPENWOP_COST_ATTRIBUTE_NAMES = [
|
|
29
|
+
'openwop.cost.tokens.input',
|
|
30
|
+
'openwop.cost.tokens.output',
|
|
31
|
+
'openwop.cost.tokens.total',
|
|
32
|
+
'openwop.cost.usd',
|
|
33
|
+
'openwop.cost.currency',
|
|
34
|
+
'openwop.cost.estimated',
|
|
35
|
+
'openwop.cost.provider',
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
/** Union of the canonical attribute names. Useful for typed
|
|
39
|
+
* sanitizer outputs in callers that pin the shape. */
|
|
40
|
+
export type OpenwopCostAttributeName = (typeof OPENWOP_COST_ATTRIBUTE_NAMES)[number];
|
|
41
|
+
|
|
42
|
+
const ALLOWLIST: ReadonlySet<string> = new Set<string>(OPENWOP_COST_ATTRIBUTE_NAMES);
|
|
43
|
+
|
|
44
|
+
/** Pure-function sanitizer. Returns a NEW object containing only
|
|
45
|
+
* allowlisted keys with primitive-typed values (number / string /
|
|
46
|
+
* boolean). Drops anything else — non-allowlisted keys, nested
|
|
47
|
+
* objects, arrays, functions, symbols, null, undefined.
|
|
48
|
+
*
|
|
49
|
+
* Callers SHOULD invoke this immediately before writing cost attrs
|
|
50
|
+
* to an OTel span:
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { sanitizeCostAttributes } from '@openwop/openwop';
|
|
54
|
+
* for (const [k, v] of Object.entries(sanitizeCostAttributes(rawAttrs))) {
|
|
55
|
+
* span.setAttribute(k, v);
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* The reference workflow-engine host wires this via
|
|
60
|
+
* `apps/workflow-engine/backend/typescript/src/observability/costEmitter.ts`. */
|
|
61
|
+
export function sanitizeCostAttributes(
|
|
62
|
+
input: Record<string, unknown>,
|
|
63
|
+
): Record<string, number | string | boolean> {
|
|
64
|
+
const out: Record<string, number | string | boolean> = {};
|
|
65
|
+
for (const [key, value] of Object.entries(input)) {
|
|
66
|
+
if (!ALLOWLIST.has(key)) continue;
|
|
67
|
+
if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') {
|
|
68
|
+
out[key] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed helpers for the `agent.*` event family (RFC 0002 §B + RFC 0024).
|
|
3
|
+
*
|
|
4
|
+
* Two layers:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Type guards** (`isAgentReasoned`, `isAgentReasoningDelta`, etc.)
|
|
7
|
+
* — discriminator-based predicates that narrow `RunEventDoc` to a
|
|
8
|
+
* `TypedRunEvent<TPayload>` inside the guarded branch. Use these
|
|
9
|
+
* when you're iterating events yourself (e.g., inside a
|
|
10
|
+
* `for await (const ev of streamEvents(...))` loop) and want
|
|
11
|
+
* compile-time-narrowed access to the payload.
|
|
12
|
+
*
|
|
13
|
+
* 2. **High-level subscription helper** (`subscribeToAgentReasoning`)
|
|
14
|
+
* — fan-outs the `streamEvents()` generator into typed callbacks
|
|
15
|
+
* (`onDelta`, `onClosed`). Composes with the existing
|
|
16
|
+
* `streamEvents` SSE consumer; cleanup via the returned
|
|
17
|
+
* `Unsubscribe` function aborts the underlying fetch.
|
|
18
|
+
*
|
|
19
|
+
* Forward-compat: `RunEventDoc.type` is intentionally `string`-typed
|
|
20
|
+
* (not a closed union) per `COMPATIBILITY.md §2.1` — consumers MUST
|
|
21
|
+
* tolerate unknown event types. The type guards here only narrow when
|
|
22
|
+
* the discriminator AND the required payload fields are present; they
|
|
23
|
+
* return `false` for malformed or unknown events.
|
|
24
|
+
*
|
|
25
|
+
* @see schemas/run-event-payloads.schema.json
|
|
26
|
+
* @see RFCS/0002-agent-identity-and-reasoning-events.md
|
|
27
|
+
* @see RFCS/0024-agent-reasoning-streaming.md
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { streamEvents, type EventsStreamContext, type EventsStreamOptions } from './sse.js';
|
|
31
|
+
import type {
|
|
32
|
+
AgentDecidedPayload,
|
|
33
|
+
AgentHandoffPayload,
|
|
34
|
+
AgentReasonedPayload,
|
|
35
|
+
AgentReasoningDeltaPayload,
|
|
36
|
+
AgentToolCalledPayload,
|
|
37
|
+
AgentToolReturnedPayload,
|
|
38
|
+
RunEventDoc,
|
|
39
|
+
TypedRunEvent,
|
|
40
|
+
} from './types.js';
|
|
41
|
+
|
|
42
|
+
// ─── Type guards ────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function hasStringField(
|
|
45
|
+
payload: unknown,
|
|
46
|
+
field: string,
|
|
47
|
+
): payload is Record<string, unknown> {
|
|
48
|
+
return (
|
|
49
|
+
payload !== null &&
|
|
50
|
+
typeof payload === 'object' &&
|
|
51
|
+
typeof (payload as Record<string, unknown>)[field] === 'string'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** `agent.reasoned` (RFC 0002 §B). Narrows when `type` matches AND
|
|
56
|
+
* payload carries the required `agentId` + `reasoning` strings. */
|
|
57
|
+
export function isAgentReasoned(
|
|
58
|
+
ev: RunEventDoc,
|
|
59
|
+
): ev is TypedRunEvent<AgentReasonedPayload> {
|
|
60
|
+
return (
|
|
61
|
+
ev.type === 'agent.reasoned' &&
|
|
62
|
+
hasStringField(ev.payload, 'agentId') &&
|
|
63
|
+
hasStringField(ev.payload, 'reasoning')
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `agent.reasoning.delta` (RFC 0024). Narrows when `type` matches AND
|
|
68
|
+
* payload carries the required `agentId` + `delta` strings + numeric
|
|
69
|
+
* `sequence`. */
|
|
70
|
+
export function isAgentReasoningDelta(
|
|
71
|
+
ev: RunEventDoc,
|
|
72
|
+
): ev is TypedRunEvent<AgentReasoningDeltaPayload> {
|
|
73
|
+
if (ev.type !== 'agent.reasoning.delta') return false;
|
|
74
|
+
if (!hasStringField(ev.payload, 'agentId')) return false;
|
|
75
|
+
if (!hasStringField(ev.payload, 'delta')) return false;
|
|
76
|
+
const seq = (ev.payload as Record<string, unknown>).sequence;
|
|
77
|
+
return typeof seq === 'number' && Number.isInteger(seq) && seq >= 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `agent.toolCalled` (RFC 0002 §B). */
|
|
81
|
+
export function isAgentToolCalled(
|
|
82
|
+
ev: RunEventDoc,
|
|
83
|
+
): ev is TypedRunEvent<AgentToolCalledPayload> {
|
|
84
|
+
return (
|
|
85
|
+
ev.type === 'agent.toolCalled' &&
|
|
86
|
+
hasStringField(ev.payload, 'agentId') &&
|
|
87
|
+
hasStringField(ev.payload, 'toolName') &&
|
|
88
|
+
hasStringField(ev.payload, 'callId')
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** `agent.toolReturned` (RFC 0002 §B). Pairs with `agent.toolCalled`
|
|
93
|
+
* via `callId`; `outcome` and `error` are mutually exclusive but the
|
|
94
|
+
* guard doesn't enforce that (callers inspect after narrowing). */
|
|
95
|
+
export function isAgentToolReturned(
|
|
96
|
+
ev: RunEventDoc,
|
|
97
|
+
): ev is TypedRunEvent<AgentToolReturnedPayload> {
|
|
98
|
+
return (
|
|
99
|
+
ev.type === 'agent.toolReturned' &&
|
|
100
|
+
hasStringField(ev.payload, 'agentId') &&
|
|
101
|
+
hasStringField(ev.payload, 'toolName') &&
|
|
102
|
+
hasStringField(ev.payload, 'callId')
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `agent.handoff` (RFC 0002 §B). Note the distinct field names —
|
|
107
|
+
* `fromAgentId` / `toAgentId`, NOT a single `agentId`. */
|
|
108
|
+
export function isAgentHandoff(
|
|
109
|
+
ev: RunEventDoc,
|
|
110
|
+
): ev is TypedRunEvent<AgentHandoffPayload> {
|
|
111
|
+
return (
|
|
112
|
+
ev.type === 'agent.handoff' &&
|
|
113
|
+
hasStringField(ev.payload, 'fromAgentId') &&
|
|
114
|
+
hasStringField(ev.payload, 'toAgentId')
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `agent.decided` (RFC 0002 §B). `decision` is `unknown` per the
|
|
119
|
+
* schema (host-specific shape); guard only validates `agentId` +
|
|
120
|
+
* the presence of a `decision` key. */
|
|
121
|
+
export function isAgentDecided(
|
|
122
|
+
ev: RunEventDoc,
|
|
123
|
+
): ev is TypedRunEvent<AgentDecidedPayload> {
|
|
124
|
+
return (
|
|
125
|
+
ev.type === 'agent.decided' &&
|
|
126
|
+
hasStringField(ev.payload, 'agentId') &&
|
|
127
|
+
ev.payload !== null &&
|
|
128
|
+
typeof ev.payload === 'object' &&
|
|
129
|
+
'decision' in (ev.payload as Record<string, unknown>)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── High-level subscription helper ─────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
/** Returned by {@link subscribeToAgentReasoning}. Call to cancel the
|
|
136
|
+
* underlying SSE subscription. Idempotent — repeated calls are no-ops. */
|
|
137
|
+
export type Unsubscribe = () => void;
|
|
138
|
+
|
|
139
|
+
/** Per-callback signatures for {@link subscribeToAgentReasoning}. All
|
|
140
|
+
* callbacks are optional; the helper only invokes those provided.
|
|
141
|
+
* Callback exceptions are caught + reported via `onError` (when
|
|
142
|
+
* provided) so one bad handler doesn't tear down the stream. */
|
|
143
|
+
export interface AgentReasoningCallbacks {
|
|
144
|
+
/** Fired for each `agent.reasoning.delta` event in arrival order
|
|
145
|
+
* (RFC 0024). `sequence` starts at 0 and increments by 1 per delta
|
|
146
|
+
* within a single reasoning block. */
|
|
147
|
+
onDelta?: (payload: AgentReasoningDeltaPayload, ev: TypedRunEvent<AgentReasoningDeltaPayload>) => void | Promise<void>;
|
|
148
|
+
/** Fired once per closed reasoning block with the full
|
|
149
|
+
* authoritative content. Consumers that only care about the final
|
|
150
|
+
* trace can subscribe just to this. */
|
|
151
|
+
onClosed?: (payload: AgentReasonedPayload, ev: TypedRunEvent<AgentReasonedPayload>) => void | Promise<void>;
|
|
152
|
+
/** Fired when the SSE stream ends cleanly (server closed or run
|
|
153
|
+
* reached terminal status). */
|
|
154
|
+
onEnd?: () => void;
|
|
155
|
+
/** Fired when the stream fails OR a callback throws. The helper
|
|
156
|
+
* catches handler exceptions so a thrown error in `onDelta` doesn't
|
|
157
|
+
* tear down the whole subscription; the original error is surfaced
|
|
158
|
+
* here. */
|
|
159
|
+
onError?: (err: Error) => void;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Subscribe to the reasoning event sub-stream for a single run.
|
|
164
|
+
* Convenience wrapper over `streamEvents()` that dispatches the two
|
|
165
|
+
* RFC 0024 event types into typed callbacks.
|
|
166
|
+
*
|
|
167
|
+
* Returns immediately; the SSE consumption runs as a detached async
|
|
168
|
+
* task in the background. Call the returned `Unsubscribe` to cancel.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* const stop = subscribeToAgentReasoning(ctx, runId, {
|
|
173
|
+
* onDelta: ({ delta, sequence }) => process.stdout.write(delta),
|
|
174
|
+
* onClosed: ({ reasoning }) => console.log('\nfinal:', reasoning),
|
|
175
|
+
* });
|
|
176
|
+
* // ...later
|
|
177
|
+
* stop();
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
export function subscribeToAgentReasoning(
|
|
181
|
+
ctx: EventsStreamContext,
|
|
182
|
+
runId: string,
|
|
183
|
+
callbacks: AgentReasoningCallbacks,
|
|
184
|
+
options: Omit<EventsStreamOptions, 'signal'> = {},
|
|
185
|
+
): Unsubscribe {
|
|
186
|
+
const abort = new AbortController();
|
|
187
|
+
let stopped = false;
|
|
188
|
+
|
|
189
|
+
const stop: Unsubscribe = () => {
|
|
190
|
+
if (stopped) return;
|
|
191
|
+
stopped = true;
|
|
192
|
+
abort.abort();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
void (async () => {
|
|
196
|
+
try {
|
|
197
|
+
// streamModes default to 'updates' — agent.* events flow there
|
|
198
|
+
// per `spec/v1/stream-modes.md`.
|
|
199
|
+
const events = streamEvents(ctx, runId, {
|
|
200
|
+
...options,
|
|
201
|
+
streamMode: options.streamMode ?? 'updates',
|
|
202
|
+
signal: abort.signal,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
for await (const ev of events) {
|
|
206
|
+
if (stopped) break;
|
|
207
|
+
try {
|
|
208
|
+
if (isAgentReasoningDelta(ev)) {
|
|
209
|
+
await callbacks.onDelta?.(ev.payload, ev);
|
|
210
|
+
} else if (isAgentReasoned(ev)) {
|
|
211
|
+
await callbacks.onClosed?.(ev.payload, ev);
|
|
212
|
+
}
|
|
213
|
+
// Other event types ignored — forward-compat per
|
|
214
|
+
// COMPATIBILITY.md §2.1. Consumers that want them iterate
|
|
215
|
+
// streamEvents() directly.
|
|
216
|
+
} catch (handlerErr) {
|
|
217
|
+
// Handler exceptions: surface via onError but DO NOT tear
|
|
218
|
+
// down the stream. One bad handler shouldn't kill the rest.
|
|
219
|
+
callbacks.onError?.(
|
|
220
|
+
handlerErr instanceof Error
|
|
221
|
+
? handlerErr
|
|
222
|
+
: new Error(String(handlerErr)),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (!stopped) callbacks.onEnd?.();
|
|
227
|
+
} catch (streamErr) {
|
|
228
|
+
if (stopped) return; // intentional cancellation
|
|
229
|
+
callbacks.onError?.(
|
|
230
|
+
streamErr instanceof Error
|
|
231
|
+
? streamErr
|
|
232
|
+
: new Error(String(streamErr)),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
})();
|
|
236
|
+
|
|
237
|
+
return stop;
|
|
238
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -28,20 +28,61 @@ export type {
|
|
|
28
28
|
ErrorEnvelope,
|
|
29
29
|
ForkRunRequest,
|
|
30
30
|
ForkRunResponse,
|
|
31
|
+
DebugBundle,
|
|
32
|
+
DebugBundleOptions,
|
|
33
|
+
RegisterWebhookRequest,
|
|
34
|
+
RegisterWebhookResponse,
|
|
31
35
|
InterruptByTokenInspection,
|
|
36
|
+
PauseRunRequest,
|
|
37
|
+
PauseRunResponse,
|
|
32
38
|
PollEventsResponse,
|
|
33
39
|
ResolveInterruptByTokenResponse,
|
|
34
40
|
ResolveInterruptRequest,
|
|
35
41
|
ResolveInterruptResponse,
|
|
42
|
+
ResumeRunRequest,
|
|
43
|
+
ResumeRunResponse,
|
|
36
44
|
RunConfigurable,
|
|
37
45
|
RunEventDoc,
|
|
38
46
|
RunSnapshot,
|
|
39
47
|
RunStatus,
|
|
40
48
|
StreamMode,
|
|
49
|
+
TypedRunEvent,
|
|
50
|
+
AgentReasonedPayload,
|
|
51
|
+
AgentReasoningDeltaPayload,
|
|
52
|
+
AgentToolCalledPayload,
|
|
53
|
+
AgentToolReturnedPayload,
|
|
54
|
+
AgentHandoffPayload,
|
|
55
|
+
AgentDecidedPayload,
|
|
56
|
+
// RFC 0027 + RFC 0028 — Prompt library (spec/v1/prompts.md)
|
|
57
|
+
GetPromptRequest,
|
|
58
|
+
ListPromptsRequest,
|
|
59
|
+
ListPromptsResponse,
|
|
60
|
+
PromptKind,
|
|
61
|
+
PromptRef,
|
|
62
|
+
PromptTemplate,
|
|
63
|
+
PromptVariable,
|
|
64
|
+
RenderPromptRequest,
|
|
65
|
+
RenderPromptResponse,
|
|
41
66
|
} from './types.js';
|
|
42
67
|
export { streamEvents } from './sse.js';
|
|
43
68
|
export type { EventsStreamContext, EventsStreamOptions } from './sse.js';
|
|
44
69
|
|
|
70
|
+
// RFC 0002 + RFC 0024 typed event helpers — type guards over `RunEventDoc`
|
|
71
|
+
// plus a high-level streaming-reasoning subscription helper.
|
|
72
|
+
export {
|
|
73
|
+
isAgentReasoned,
|
|
74
|
+
isAgentReasoningDelta,
|
|
75
|
+
isAgentToolCalled,
|
|
76
|
+
isAgentToolReturned,
|
|
77
|
+
isAgentHandoff,
|
|
78
|
+
isAgentDecided,
|
|
79
|
+
subscribeToAgentReasoning,
|
|
80
|
+
} from './event-helpers.js';
|
|
81
|
+
export type {
|
|
82
|
+
AgentReasoningCallbacks,
|
|
83
|
+
Unsubscribe,
|
|
84
|
+
} from './event-helpers.js';
|
|
85
|
+
|
|
45
86
|
// Run-status + run-error helpers (added in 1). Forward-compat predicates
|
|
46
87
|
// over the wire-format unions declared above; full design + rationale in
|
|
47
88
|
// `run-helpers.ts`.
|
|
@@ -61,3 +102,58 @@ export type {
|
|
|
61
102
|
RunErrorCode,
|
|
62
103
|
RunError,
|
|
63
104
|
} from './run-helpers.js';
|
|
105
|
+
|
|
106
|
+
// Cost-attribution allowlist + sanitizer helpers
|
|
107
|
+
// (`spec/v1/observability.md §"Cost attribution attributes"`). Single
|
|
108
|
+
// source of truth for the canonical attribute names so independent
|
|
109
|
+
// hosts share one list instead of each re-deriving it.
|
|
110
|
+
export {
|
|
111
|
+
OPENWOP_COST_ATTRIBUTE_NAMES,
|
|
112
|
+
sanitizeCostAttributes,
|
|
113
|
+
} from './cost-attribution.js';
|
|
114
|
+
export type { OpenwopCostAttributeName } from './cost-attribution.js';
|
|
115
|
+
|
|
116
|
+
// Webhook-delivery verification helpers (SDK-3 close-out 2026-05-15).
|
|
117
|
+
// HMAC-SHA256 + timestamp freshness window verification per
|
|
118
|
+
// spec/v1/webhooks.md §"Signature recipe". Receivers MUST verify both
|
|
119
|
+
// the HMAC AND the timestamp to defeat replay attacks.
|
|
120
|
+
export {
|
|
121
|
+
DEFAULT_WEBHOOK_FRESHNESS_WINDOW_SECONDS,
|
|
122
|
+
verifyWebhookSignature,
|
|
123
|
+
signWebhookDelivery,
|
|
124
|
+
} from './webhook-helpers.js';
|
|
125
|
+
export type {
|
|
126
|
+
VerifyWebhookSignatureOptions,
|
|
127
|
+
VerifyWebhookOutcome,
|
|
128
|
+
} from './webhook-helpers.js';
|
|
129
|
+
|
|
130
|
+
// Public-registry read helpers (SDK-5 close-out 2026-05-15). Read-only
|
|
131
|
+
// typed client for the public node-pack registry at packs.openwop.dev
|
|
132
|
+
// per spec/v1/registry-operations.md.
|
|
133
|
+
export { RegistryClient } from './registry-helpers.js';
|
|
134
|
+
export type {
|
|
135
|
+
RegistryClientOptions,
|
|
136
|
+
RegistryDiscovery,
|
|
137
|
+
RegistryIndex,
|
|
138
|
+
RegistryIndexEntry,
|
|
139
|
+
RegistryPackMetadata,
|
|
140
|
+
RegistryVersionManifest,
|
|
141
|
+
} from './registry-helpers.js';
|
|
142
|
+
|
|
143
|
+
// AI Envelope types (DRAFT v1.x — spec/v1/ai-envelope.md). Inbound LLM-emission
|
|
144
|
+
// envelope, distinct from RunEventDoc (outbound) and ErrorEnvelope (host HTTP).
|
|
145
|
+
export type {
|
|
146
|
+
AIEnvelope,
|
|
147
|
+
AIEnvelopeErrorPayload,
|
|
148
|
+
ClarificationRequestPayload,
|
|
149
|
+
EnvelopeContract,
|
|
150
|
+
EnvelopeContractRefusal,
|
|
151
|
+
EnvelopeContractsCapability,
|
|
152
|
+
EnvelopeMeta,
|
|
153
|
+
EnvelopeOutcome,
|
|
154
|
+
EnvelopeStrictness,
|
|
155
|
+
PartialInfo,
|
|
156
|
+
SchemaRequestPayload,
|
|
157
|
+
SchemaResponsePayload,
|
|
158
|
+
ValidationDetail,
|
|
159
|
+
} from './types.js';
|