@openwop/openwop 1.8.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +64 -117
  2. package/dist/client.d.ts +131 -245
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +233 -440
  5. package/dist/client.js.map +1 -1
  6. package/dist/cost-attribution.d.ts +2 -2
  7. package/dist/cost-attribution.js +2 -2
  8. package/dist/envelope-directive.d.ts +1 -1
  9. package/dist/envelope-directive.js +1 -1
  10. package/dist/event-helpers.js +1 -1
  11. package/dist/event-helpers.js.map +1 -1
  12. package/dist/generated.d.ts +17 -0
  13. package/dist/generated.d.ts.map +1 -0
  14. package/dist/generated.js +311 -0
  15. package/dist/generated.js.map +1 -0
  16. package/dist/index.d.ts +16 -16
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +26 -53
  19. package/dist/index.js.map +1 -1
  20. package/dist/run-helpers.d.ts +20 -21
  21. package/dist/run-helpers.d.ts.map +1 -1
  22. package/dist/run-helpers.js +23 -72
  23. package/dist/run-helpers.js.map +1 -1
  24. package/dist/sse.d.ts +33 -15
  25. package/dist/sse.d.ts.map +1 -1
  26. package/dist/sse.js +28 -30
  27. package/dist/sse.js.map +1 -1
  28. package/dist/types.d.ts +253 -559
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/types.js.map +1 -1
  31. package/dist/webhook-header-families.d.ts +42 -0
  32. package/dist/webhook-header-families.d.ts.map +1 -0
  33. package/dist/webhook-header-families.js +58 -0
  34. package/dist/webhook-header-families.js.map +1 -0
  35. package/dist/webhook-helpers.browser.d.ts +15 -28
  36. package/dist/webhook-helpers.browser.d.ts.map +1 -1
  37. package/dist/webhook-helpers.browser.js +16 -27
  38. package/dist/webhook-helpers.browser.js.map +1 -1
  39. package/dist/webhook-helpers.d.ts +48 -32
  40. package/dist/webhook-helpers.d.ts.map +1 -1
  41. package/dist/webhook-helpers.js +52 -37
  42. package/dist/webhook-helpers.js.map +1 -1
  43. package/package.json +6 -4
  44. package/src/client.ts +255 -454
  45. package/src/cost-attribution.ts +2 -2
  46. package/src/envelope-directive.ts +1 -1
  47. package/src/event-helpers.ts +1 -1
  48. package/src/generated.ts +322 -0
  49. package/src/index.ts +78 -105
  50. package/src/run-helpers.ts +27 -85
  51. package/src/sse.ts +63 -42
  52. package/src/types.ts +268 -603
  53. package/src/webhook-header-families.ts +78 -0
  54. package/src/webhook-helpers.browser.ts +24 -28
  55. package/src/webhook-helpers.ts +87 -39
  56. package/dist/registry-helpers.d.ts +0 -118
  57. package/dist/registry-helpers.d.ts.map +0 -1
  58. package/dist/registry-helpers.js +0 -82
  59. package/dist/registry-helpers.js.map +0 -1
  60. package/src/registry-helpers.ts +0 -173
package/src/client.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  /**
2
- * OpenwopClient — typed HTTP client for the openwop REST surface.
2
+ * OpenwopClient — typed HTTP client for the OpenWOP v2 REST surface.
3
3
  *
4
- * Hand-authored. Each method maps 1:1 to a documented endpoint in
5
- * ../../api/openapi.yaml. Request/response types live in ./types.ts.
4
+ * Hand-authored. Each method maps 1:1 to an operation in
5
+ * `spec/v2/path-manifest.json` (generated from `api/v2/openapi.yaml`):
6
+ * bare origin, unversioned path keys, negotiation by the `OpenWOP-Version`
7
+ * request header (RFC 0172 §A). Request/response types live in ./types.ts;
8
+ * the error-code union in ./generated.ts.
6
9
  *
7
- * Auth: a single bearer-style API key, supplied at construction. See
8
- * ../../auth.md for credential format.
10
+ * Auth: a single bearer-style API key, supplied at construction.
9
11
  */
10
12
 
11
- import { streamEvents, type EventsStreamOptions } from './sse.js';
13
+ import { streamEvents, streamHostEvents, type EventsStreamOptions, type HostEventsStreamOptions } from './sse.js';
12
14
  import {
13
15
  WopError,
14
16
  type AuditVerifyResult,
@@ -24,19 +26,19 @@ import {
24
26
  type Capabilities,
25
27
  type CancelRunRequest,
26
28
  type CancelRunResponse,
29
+ type CompensationProjection,
27
30
  type CreateRunRequest,
28
31
  type CreateRunResponse,
32
+ type EffectLedgerProjection,
33
+ type EffectSeamManifest,
29
34
  type ErrorEnvelope,
30
35
  type ForkRunRequest,
31
36
  type ForkRunResponse,
32
37
  type Annotation,
33
38
  type CreateAnnotationRequest,
34
- type WorkspaceFile,
35
- type PutWorkspaceFileRequest,
36
39
  type GetPromptRequest,
40
+ type HostEventDoc,
37
41
  type InterruptByTokenInspection,
38
- type DebugBundle,
39
- type DebugBundleOptions,
40
42
  type ListPromptsRequest,
41
43
  type ListPromptsResponse,
42
44
  type PromptTemplate,
@@ -67,18 +69,31 @@ import {
67
69
  type CompactToolDescriptor,
68
70
  type AgentDeployment,
69
71
  type AgentDeploymentTransition,
70
- type CreateUserAgentRequest,
71
- type UserAgentRecord,
72
- type AgentPackRegistryResponse,
73
- type InstallAgentPackRequest,
74
- type InstallAgentPackResponse,
75
72
  } from './types.js';
76
73
 
74
+ /** The protocol major this SDK implements; the default for {@link OpenwopClientOptions.major}. */
75
+ export const SDK_PROTOCOL_MAJOR = 2;
76
+
77
+ /** Renders the `OpenWOP-Version` request value for a major: `<major>.0` (the OpenAPI grammar is `<major>.<minor>`). */
78
+ export function protocolVersionHeader(major: number): string {
79
+ if (!Number.isInteger(major) || major < 0) {
80
+ throw new TypeError(`OpenwopClient: major must be a non-negative integer (got ${String(major)})`);
81
+ }
82
+ return `${major}.0`;
83
+ }
84
+
77
85
  export interface OpenwopClientOptions {
78
86
  /** Base URL of the openwop server, e.g., `https://api.example.com`. Trailing slash optional. */
79
87
  readonly baseUrl: string;
80
- /** API key (bearer-style). See auth.md. */
88
+ /** API key (bearer-style). */
81
89
  readonly apiKey: string;
90
+ /**
91
+ * The protocol major to negotiate (RFC 0172 §A.3). Every request carries
92
+ * `OpenWOP-Version: <major>.0`; a host that does not list the major answers
93
+ * `406 protocol_version_unsupported` with `details.protocolVersions[]`.
94
+ * Default {@link SDK_PROTOCOL_MAJOR} (2).
95
+ */
96
+ readonly major?: number;
82
97
  /** Optional fetch implementation override (test injection). Defaults to global `fetch`. */
83
98
  readonly fetch?: typeof fetch;
84
99
  /** Default `Accept-Language` to send. Optional. */
@@ -86,9 +101,9 @@ export interface OpenwopClientOptions {
86
101
  }
87
102
 
88
103
  export interface MutationOptions {
89
- /** RFC-spec'd Idempotency-Key for at-most-once mutation semantics. */
104
+ /** `Idempotency-Key` for at-most-once mutation semantics (idempotency.md Layer 1). */
90
105
  readonly idempotencyKey?: string;
91
- /** Optional X-Dedup hint for cross-host claim coordination on POST /v1/runs. */
106
+ /** `OpenWOP-Dedup: enforce` the host rejects a duplicate `(tenantId, scopeId)` with `409 run_already_active` (runs.md §Create). */
92
107
  readonly dedup?: 'enforce';
93
108
  }
94
109
 
@@ -105,6 +120,12 @@ export class OpenwopClient {
105
120
  readonly #apiKey: string;
106
121
  readonly #fetch: typeof fetch;
107
122
  readonly #acceptLanguage: string | undefined;
123
+ readonly #versionHeader: string;
124
+
125
+ /** The `OpenWOP-Version` value this client sends on every request. */
126
+ get protocolVersion(): string {
127
+ return this.#versionHeader;
128
+ }
108
129
 
109
130
  constructor(opts: OpenwopClientOptions) {
110
131
  if (!opts.baseUrl) throw new TypeError('OpenwopClient: baseUrl is required');
@@ -113,69 +134,53 @@ export class OpenwopClient {
113
134
  this.#apiKey = opts.apiKey;
114
135
  this.#fetch = opts.fetch ?? fetch;
115
136
  this.#acceptLanguage = opts.acceptLanguage;
137
+ this.#versionHeader = protocolVersionHeader(opts.major ?? SDK_PROTOCOL_MAJOR);
116
138
  }
117
139
 
118
140
  // ── Discovery ────────────────────────────────────────────────────────
119
141
  readonly discovery = {
142
+ /**
143
+ * `GET /.well-known/openwop` — one resource whose representation the
144
+ * `OpenWOP-Version` header selects (capabilities.md §1): with this
145
+ * client's major the host returns the closed v2 root.
146
+ */
120
147
  capabilities: (): Promise<Capabilities> =>
121
148
  this.#request<Capabilities>({ method: 'GET', path: '/.well-known/openwop' }, false),
122
149
 
150
+ /** `GET /openapi.json` — the self-describing OpenAPI 3.1 document. */
123
151
  openapi: (): Promise<unknown> =>
124
- this.#request<unknown>({ method: 'GET', path: '/v1/openapi.json' }, false),
152
+ this.#request<unknown>({ method: 'GET', path: '/openapi.json' }, false),
125
153
  };
126
154
 
127
155
  // ── Workflows ────────────────────────────────────────────────────────
128
156
  readonly workflows = {
157
+ /** `GET /workflows/{workflowId}` */
129
158
  get: (workflowId: string): Promise<unknown> =>
130
159
  this.#request<unknown>({
131
160
  method: 'GET',
132
- path: `/v1/workflows/${encodeURIComponent(workflowId)}`,
161
+ path: `/workflows/${encodeURIComponent(workflowId)}`,
133
162
  }),
134
163
  };
135
164
 
136
165
  // ── Runs ─────────────────────────────────────────────────────────────
137
166
  readonly runs = {
167
+ /** `POST /runs` — the body is closed at the composition (runs.md §Create). */
138
168
  create: (body: CreateRunRequest, opts: MutationOptions = {}): Promise<CreateRunResponse> =>
139
169
  this.#request<CreateRunResponse>({
140
170
  method: 'POST',
141
- path: '/v1/runs',
171
+ path: '/runs',
142
172
  body,
143
173
  headers: this.#mutationHeaders(opts),
144
174
  }),
145
175
 
176
+ /** `GET /runs/{runId}` — the snapshot (runs.md §Snapshot). */
146
177
  get: (runId: string): Promise<RunSnapshot> =>
147
178
  this.#request<RunSnapshot>({
148
179
  method: 'GET',
149
- path: `/v1/runs/${encodeURIComponent(runId)}`,
180
+ path: `/runs/${encodeURIComponent(runId)}`,
150
181
  }),
151
182
 
152
- /**
153
- * Fetch the portable JSON diagnostic export for a single run per
154
- * `spec/v1/debug-bundle.md`. The bundle's `redactionMode` reflects
155
- * the host's advertised `capabilities.compliance.defaultMode`; the
156
- * caller MUST treat masked/omitted/hashed fields as the
157
- * spec-canonical value. The `truncated` + `truncatedReason` fields
158
- * indicate the host hit its size cap.
159
- *
160
- * Returns `null` when the host doesn't advertise
161
- * `capabilities.debugBundle.supported: true` (the endpoint returns
162
- * 404 in that case per `debug-bundle.md` §"Authorization").
163
- */
164
- debugBundle: async (runId: string, opts: DebugBundleOptions = {}): Promise<DebugBundle | null> => {
165
- const params = new URLSearchParams();
166
- if (opts.maxEvents !== undefined) params.set('maxEvents', String(opts.maxEvents));
167
- const query = params.toString();
168
- const path = `/v1/runs/${encodeURIComponent(runId)}/debug-bundle${query ? `?${query}` : ''}`;
169
- try {
170
- return await this.#request<DebugBundle>({ method: 'GET', path });
171
- } catch (err) {
172
- // Host doesn't advertise the capability → 404. Surface as null so callers
173
- // can branch on capability discovery without try/catch.
174
- if (err instanceof WopError && err.status === 404) return null;
175
- throw err;
176
- }
177
- },
178
-
183
+ /** `POST /runs/{runId}/cancel` — `status` is `cancelling` or `cancelled`. */
179
184
  cancel: (
180
185
  runId: string,
181
186
  body: CancelRunRequest = {},
@@ -183,11 +188,12 @@ export class OpenwopClient {
183
188
  ): Promise<CancelRunResponse> =>
184
189
  this.#request<CancelRunResponse>({
185
190
  method: 'POST',
186
- path: `/v1/runs/${encodeURIComponent(runId)}/cancel`,
191
+ path: `/runs/${encodeURIComponent(runId)}/cancel`,
187
192
  body,
188
193
  headers: this.#mutationHeaders(opts),
189
194
  }),
190
195
 
196
+ /** `POST /runs/{runId}:pause` — `409` when the run is not pausable. */
191
197
  pause: (
192
198
  runId: string,
193
199
  body: PauseRunRequest = {},
@@ -195,11 +201,12 @@ export class OpenwopClient {
195
201
  ): Promise<PauseRunResponse> =>
196
202
  this.#request<PauseRunResponse>({
197
203
  method: 'POST',
198
- path: `/v1/runs/${encodeURIComponent(runId)}:pause`,
204
+ path: `/runs/${encodeURIComponent(runId)}:pause`,
199
205
  body,
200
206
  headers: this.#mutationHeaders(opts),
201
207
  }),
202
208
 
209
+ /** `POST /runs/{runId}:resume` — `409` when the run is not paused. */
203
210
  resume: (
204
211
  runId: string,
205
212
  body: ResumeRunRequest = {},
@@ -207,19 +214,15 @@ export class OpenwopClient {
207
214
  ): Promise<ResumeRunResponse> =>
208
215
  this.#request<ResumeRunResponse>({
209
216
  method: 'POST',
210
- path: `/v1/runs/${encodeURIComponent(runId)}:resume`,
217
+ path: `/runs/${encodeURIComponent(runId)}:resume`,
211
218
  body,
212
219
  headers: this.#mutationHeaders(opts),
213
220
  }),
214
221
 
215
222
  /**
216
- * Bulk-cancel a set of in-flight runs in a single request per
217
- * `rest-endpoints.md` §"POST /v1/runs:bulk-cancel" (closes R1).
218
- * The top-level call returns 200 + per-id `results[]` whenever the
219
- * request reaches the host; partial failures surface inside the
220
- * array (each entry carries `ok: boolean` + optional `error`). Host-
221
- * defined cap on `runIds[]` length (RECOMMENDED 100); over-cap
222
- * returns `400 validation_error` with `details.maxRunIds`.
223
+ * `POST /runs:bulk-cancel` `200 { results[] }` in request order even
224
+ * when every id failed; per-id authorization yields `ok: false` with
225
+ * `run_forbidden` in that entry, never a top-level `403` (runs.md §Cancel).
223
226
  */
224
227
  bulkCancel: (
225
228
  body: BulkCancelRunsRequest,
@@ -227,11 +230,12 @@ export class OpenwopClient {
227
230
  ): Promise<BulkCancelRunsResponse> =>
228
231
  this.#request<BulkCancelRunsResponse>({
229
232
  method: 'POST',
230
- path: '/v1/runs:bulk-cancel',
233
+ path: '/runs:bulk-cancel',
231
234
  body,
232
235
  headers: this.#mutationHeaders(opts),
233
236
  }),
234
237
 
238
+ /** `POST /runs/{runId}:fork` — `mode: replay | branch` (runs.md §Fork; replay.md). */
235
239
  fork: (
236
240
  runId: string,
237
241
  body: ForkRunRequest,
@@ -239,16 +243,15 @@ export class OpenwopClient {
239
243
  ): Promise<ForkRunResponse> =>
240
244
  this.#request<ForkRunResponse>({
241
245
  method: 'POST',
242
- path: `/v1/runs/${encodeURIComponent(runId)}:fork`,
246
+ path: `/runs/${encodeURIComponent(runId)}:fork`,
243
247
  body,
244
248
  headers: this.#mutationHeaders(opts),
245
249
  }),
246
250
 
247
251
  /**
248
- * RFC 0056record a non-blocking quality annotation (rating / correction
249
- * / label / flag) on a run/event/node. Returns the persisted `Annotation`.
250
- * Throws on non-2xx (`501` when the host doesn't advertise
251
- * `capabilities.feedback.supported`).
252
+ * `POST /runs/{runId}/annotations` — a live notification, never a run
253
+ * event. Throws on non-2xx (`501` when the host doesn't advertise
254
+ * `feedback`).
252
255
  */
253
256
  createAnnotation: (
254
257
  runId: string,
@@ -257,21 +260,21 @@ export class OpenwopClient {
257
260
  ): Promise<Annotation> =>
258
261
  this.#request<Annotation>({
259
262
  method: 'POST',
260
- path: `/v1/runs/${encodeURIComponent(runId)}/annotations`,
263
+ path: `/runs/${encodeURIComponent(runId)}/annotations`,
261
264
  body,
262
265
  headers: this.#mutationHeaders(opts),
263
266
  }),
264
267
 
265
268
  /**
266
- * RFC 0056list a run's annotations (tenant-scoped). Returns `null` when
267
- * the host doesn't advertise `capabilities.feedback` (404/501), so callers
268
- * can branch on capability discovery without try/catch.
269
+ * `GET /runs/{runId}/annotations`returns `null` when the host doesn't
270
+ * advertise `feedback` (404/501), so callers can branch on capability
271
+ * discovery without try/catch.
269
272
  */
270
273
  listAnnotations: async (runId: string): Promise<readonly Annotation[] | null> => {
271
274
  try {
272
275
  const res = await this.#request<{ annotations: Annotation[] }>({
273
276
  method: 'GET',
274
- path: `/v1/runs/${encodeURIComponent(runId)}/annotations`,
277
+ path: `/runs/${encodeURIComponent(runId)}/annotations`,
275
278
  });
276
279
  return res.annotations;
277
280
  } catch (err) {
@@ -281,21 +284,17 @@ export class OpenwopClient {
281
284
  },
282
285
 
283
286
  /**
284
- * RFC 0040 §C fetch the run's immediate parent in the cross-host
285
- * composition chain. Returns `parent: null` for top-level runs;
286
- * `parent.wellKnownUrl` is set when the parent is on a different
287
- * host, so callers walk the chain one hop at a time.
288
- *
287
+ * `GET /runs/{runId}/ancestry` — the run's immediate parent in the
288
+ * cross-host composition chain; `parent: null` for top-level runs.
289
289
  * Returns `null` when the host doesn't advertise
290
- * `capabilities.multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported: true`
291
- * (the endpoint returns 404 in that case per
292
- * `spec/v1/multi-agent-execution.md` §"GET /v1/runs/{runId}/ancestry").
290
+ * `multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported`
291
+ * (the endpoint 404s).
293
292
  */
294
293
  ancestry: async (runId: string): Promise<RunAncestryResponse | null> => {
295
294
  try {
296
295
  return await this.#request<RunAncestryResponse>({
297
296
  method: 'GET',
298
- path: `/v1/runs/${encodeURIComponent(runId)}/ancestry`,
297
+ path: `/runs/${encodeURIComponent(runId)}/ancestry`,
299
298
  });
300
299
  } catch (err) {
301
300
  if (err instanceof WopError && err.status === 404) return null;
@@ -304,18 +303,15 @@ export class OpenwopClient {
304
303
  },
305
304
 
306
305
  /**
307
- * RFC 0054 — deterministic, replay-aware structured diff of two runs
308
- * (typically a run and its `:fork`). Requires `runs:read` on BOTH
309
- * `runId` and `against`. Returns `null` when the host doesn't
310
- * implement the endpoint (404 per `spec/v1/rest-endpoints.md`
311
- * §`GET /v1/runs/{runId}:diff`). `divergedAtSeq` is null + `eventDiffs`
312
- * empty when the two logs are identical.
306
+ * `GET /runs/{runId}:diff?against=` — deterministic, replay-aware diff of
307
+ * two runs; requires `runs:read` on both. OPTIONAL surface: `null` on
308
+ * `404`. Identical logs yield `divergedAtSeq: null` + empty `eventDiffs`.
313
309
  */
314
310
  diff: async (runId: string, against: string): Promise<RunDiffResponse | null> => {
315
311
  try {
316
312
  return await this.#request<RunDiffResponse>({
317
313
  method: 'GET',
318
- path: `/v1/runs/${encodeURIComponent(runId)}:diff?against=${encodeURIComponent(against)}`,
314
+ path: `/runs/${encodeURIComponent(runId)}:diff?against=${encodeURIComponent(against)}`,
319
315
  });
320
316
  } catch (err) {
321
317
  if (err instanceof WopError && err.status === 404) return null;
@@ -324,18 +320,15 @@ export class OpenwopClient {
324
320
  },
325
321
 
326
322
  /**
327
- * RFC 0081 §C — the `EvalSummary` scorecard for a terminal eval run (one
328
- * started via `runs.create({ mode: 'eval', evalSuiteRef, agentId })`):
329
- * aggregate + per-task scores, cost, latency, schema-validity, and
330
- * redaction-safe safety findings. Returns `null` when the host doesn't
331
- * advertise `capabilities.agents.evalSuite` or the run isn't an eval run
332
- * (404). Throws `409` while the run is still in progress.
323
+ * `GET /runs/{runId}/eval-summary` — the `EvalSummary` for a terminal eval
324
+ * run. `null` on `404` (not an eval run, or `agents.evalSuite`
325
+ * unadvertised); throws `409` while the run is still in progress.
333
326
  */
334
327
  evalSummary: async (runId: string): Promise<EvalSummary | null> => {
335
328
  try {
336
329
  return await this.#request<EvalSummary>({
337
330
  method: 'GET',
338
- path: `/v1/runs/${encodeURIComponent(runId)}/eval-summary`,
331
+ path: `/runs/${encodeURIComponent(runId)}/eval-summary`,
339
332
  });
340
333
  } catch (err) {
341
334
  if (err instanceof WopError && err.status === 404) return null;
@@ -344,15 +337,46 @@ export class OpenwopClient {
344
337
  },
345
338
 
346
339
  /**
347
- * Read a run-produced artifact by id (`GET /v1/runs/{runId}/artifacts/{artifactId}`).
348
- * The artifact body is implementation-defined per the host. Returns `null`
349
- * on 404 (no such artifact, or the host doesn't store artifacts).
340
+ * `GET /runs/{runId}/artifacts/{artifactId}` — an implementation-defined
341
+ * JSON object. `null` on `404`.
350
342
  */
351
343
  getArtifact: async (runId: string, artifactId: string): Promise<Record<string, unknown> | null> => {
352
344
  try {
353
345
  return await this.#request<Record<string, unknown>>({
354
346
  method: 'GET',
355
- path: `/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(artifactId)}`,
347
+ path: `/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(artifactId)}`,
348
+ });
349
+ } catch (err) {
350
+ if (err instanceof WopError && err.status === 404) return null;
351
+ throw err;
352
+ }
353
+ },
354
+
355
+ /**
356
+ * `GET /runs/{runId}/compensation` (RFC 0173 §C.1) — the compensation
357
+ * plan and attempts. Gated on `compensation`; `null` on `404`.
358
+ */
359
+ compensation: async (runId: string): Promise<CompensationProjection | null> => {
360
+ try {
361
+ return await this.#request<CompensationProjection>({
362
+ method: 'GET',
363
+ path: `/runs/${encodeURIComponent(runId)}/compensation`,
364
+ });
365
+ } catch (err) {
366
+ if (err instanceof WopError && err.status === 404) return null;
367
+ throw err;
368
+ }
369
+ },
370
+
371
+ /**
372
+ * `GET /runs/{runId}/effects` (RFC 0173 §C.2) — the Layer-2 effect ledger,
373
+ * business-identity keyed. Gated on `idempotency`; `null` on `404`.
374
+ */
375
+ effects: async (runId: string): Promise<EffectLedgerProjection | null> => {
376
+ try {
377
+ return await this.#request<EffectLedgerProjection>({
378
+ method: 'GET',
379
+ path: `/runs/${encodeURIComponent(runId)}/effects`,
356
380
  });
357
381
  } catch (err) {
358
382
  if (err instanceof WopError && err.status === 404) return null;
@@ -360,13 +384,20 @@ export class OpenwopClient {
360
384
  }
361
385
  },
362
386
 
387
+ /**
388
+ * `GET /runs/{runId}/events/poll` — the long-poll fallback (events.md
389
+ * §Poll). `afterSequence` returns events with `sequence > afterSequence`;
390
+ * omission means from the first event. The response's `lastSequence` is
391
+ * the highest sequence in the log (`-1` when empty) — feed it back as the
392
+ * next `afterSequence`.
393
+ */
363
394
  pollEvents: (
364
395
  runId: string,
365
- params: { lastSequence?: number; timeoutSeconds?: number } = {},
396
+ params: { afterSequence?: number; timeoutSeconds?: number } = {},
366
397
  ): Promise<PollEventsResponse> => {
367
398
  const search = new URLSearchParams();
368
- if (params.lastSequence !== undefined) {
369
- search.set('lastSequence', String(params.lastSequence));
399
+ if (params.afterSequence !== undefined) {
400
+ search.set('afterSequence', String(params.afterSequence));
370
401
  }
371
402
  if (params.timeoutSeconds !== undefined) {
372
403
  search.set('timeout', String(params.timeoutSeconds));
@@ -374,39 +405,57 @@ export class OpenwopClient {
374
405
  const qs = search.toString();
375
406
  return this.#request<PollEventsResponse>({
376
407
  method: 'GET',
377
- path: `/v1/runs/${encodeURIComponent(runId)}/events/poll${qs ? `?${qs}` : ''}`,
408
+ path: `/runs/${encodeURIComponent(runId)}/events/poll${qs ? `?${qs}` : ''}`,
378
409
  });
379
410
  },
380
411
 
381
412
  /**
382
- * Async-iterable SSE consumer. The connection auto-closes when the
383
- * server closes the stream (terminal run event); break out of the
384
- * loop or call `signal.abort()` to terminate early.
413
+ * `GET /runs/{runId}/events` — async-iterable SSE consumer. The
414
+ * connection auto-closes after the run's terminal event; break out of
415
+ * the loop or call `signal.abort()` to terminate early.
385
416
  */
386
417
  events: (runId: string, opts: EventsStreamOptions = {}): AsyncGenerator<RunEventDoc, void, void> =>
387
- streamEvents({ baseUrl: this.#baseUrl, apiKey: this.#apiKey }, runId, opts),
418
+ streamEvents(this.#streamContext(), runId, opts),
419
+ };
420
+
421
+ // ── Host ─────────────────────────────────────────────────────────────
422
+ readonly host = {
423
+ /**
424
+ * `GET /host/effect-seams` (RFC 0173 §C) — every outbound effect seam
425
+ * replay suppression covers. Throws `401` when unauthenticated.
426
+ */
427
+ effectSeams: (): Promise<EffectSeamManifest> =>
428
+ this.#request<EffectSeamManifest>({ method: 'GET', path: '/host/effect-seams' }),
429
+
430
+ /**
431
+ * `GET /host/events` — the `hostEvents` channel (heartbeat messages) as
432
+ * SSE; content-free of run data. A host MAY declare another address
433
+ * under `heartbeat.deliveryChannel` — pass it as `opts.path`.
434
+ */
435
+ events: (opts: HostEventsStreamOptions = {}): AsyncGenerator<HostEventDoc, void, void> =>
436
+ streamHostEvents(this.#streamContext(), opts),
388
437
  };
389
438
 
390
439
  // ── Manifest-agent inventory (RFC 0072 §A) ───────────────────────────
391
- // Read-only. Gated on `capabilities.agents.manifestRuntime`; both methods
392
- // return `null` when the host doesn't advertise it (the endpoints 404).
393
- // Dispatch is not here: a manifest agent runs as a `runs.create` whose
394
- // workflow node pins it via `WorkflowNode.agent` (RFC 0072 §B).
440
+ // Read-only. Gated on `agents`; the methods return `null` when the host
441
+ // doesn't advertise it (the endpoints 404).
395
442
  readonly agents = {
443
+ /** `GET /agents` */
396
444
  list: async (): Promise<AgentInventoryResponse | null> => {
397
445
  try {
398
- return await this.#request<AgentInventoryResponse>({ method: 'GET', path: '/v1/agents' });
446
+ return await this.#request<AgentInventoryResponse>({ method: 'GET', path: '/agents' });
399
447
  } catch (err) {
400
448
  if (err instanceof WopError && err.status === 404) return null;
401
449
  throw err;
402
450
  }
403
451
  },
404
452
 
453
+ /** `GET /agents/{agentId}` */
405
454
  get: async (agentId: string): Promise<AgentInventoryEntry | null> => {
406
455
  try {
407
456
  return await this.#request<AgentInventoryEntry>({
408
457
  method: 'GET',
409
- path: `/v1/agents/${encodeURIComponent(agentId)}`,
458
+ path: `/agents/${encodeURIComponent(agentId)}`,
410
459
  });
411
460
  } catch (err) {
412
461
  if (err instanceof WopError && err.status === 404) return null;
@@ -414,17 +463,12 @@ export class OpenwopClient {
414
463
  }
415
464
  },
416
465
 
417
- /**
418
- * RFC 0082 §C/§E — list a manifest agent's deployment records (per-(agentId,
419
- * version) lifecycle state + channels + canary + rollback pointer). Returns
420
- * `null` when the host doesn't advertise `capabilities.agents.deployment`
421
- * (the endpoint 404s).
422
- */
466
+ /** `GET /agents/{agentId}/deployments` (RFC 0082 §C/§E) — `null` when unadvertised. */
423
467
  listDeployments: async (agentId: string): Promise<readonly AgentDeployment[] | null> => {
424
468
  try {
425
469
  return await this.#request<AgentDeployment[]>({
426
470
  method: 'GET',
427
- path: `/v1/agents/${encodeURIComponent(agentId)}/deployments`,
471
+ path: `/agents/${encodeURIComponent(agentId)}/deployments`,
428
472
  });
429
473
  } catch (err) {
430
474
  if (err instanceof WopError && err.status === 404) return null;
@@ -432,14 +476,7 @@ export class OpenwopClient {
432
476
  }
433
477
  },
434
478
 
435
- /**
436
- * RFC 0082 §E — request a deployment state transition (promote / pause /
437
- * deprecate / rollback / adjust-canary). The host authorizes fail-closed
438
- * against the RFC 0049 `deploy:*` scope, runs any RFC 0051 approvalGate, and
439
- * enforces RFC 0081 `requiredEval` before emitting `deployment.promoted`.
440
- * Returns the updated deployment record. Throws on non-2xx (`403` fail-closed
441
- * / `eval_gate_unmet`; `400` `no_active_deployment` / unsupported state).
442
- */
479
+ /** `POST /agents/{agentId}/deployments` (RFC 0082 §E) — a deployment state transition. */
443
480
  transitionDeployment: (
444
481
  agentId: string,
445
482
  body: AgentDeploymentTransition,
@@ -447,34 +484,27 @@ export class OpenwopClient {
447
484
  ): Promise<AgentDeployment> =>
448
485
  this.#request<AgentDeployment>({
449
486
  method: 'POST',
450
- path: `/v1/agents/${encodeURIComponent(agentId)}/deployments`,
487
+ path: `/agents/${encodeURIComponent(agentId)}/deployments`,
451
488
  body,
452
489
  headers: this.#mutationHeaders(opts),
453
490
  }),
454
491
 
455
- /**
456
- * RFC 0086 §B — list the standing agent roster (named instances + their
457
- * workflow portfolios) visible to the caller. Returns `null` when the host
458
- * doesn't advertise `capabilities.agents.roster` (the endpoint 404s).
459
- */
492
+ /** `GET /agents/roster` (RFC 0086 §B) — `null` when unadvertised. */
460
493
  listRoster: async (): Promise<AgentRosterResponse | null> => {
461
494
  try {
462
- return await this.#request<AgentRosterResponse>({ method: 'GET', path: '/v1/agents/roster' });
495
+ return await this.#request<AgentRosterResponse>({ method: 'GET', path: '/agents/roster' });
463
496
  } catch (err) {
464
497
  if (err instanceof WopError && err.status === 404) return null;
465
498
  throw err;
466
499
  }
467
500
  },
468
501
 
469
- /**
470
- * RFC 0086 §B — return one standing roster entry. Returns `null` on 404
471
- * (no such entry, cross-tenant, or the capability is unadvertised).
472
- */
502
+ /** `GET /agents/roster/{rosterId}` (RFC 0086 §B) — `null` on `404`. */
473
503
  getRosterEntry: async (rosterId: string): Promise<AgentRosterEntry | null> => {
474
504
  try {
475
505
  return await this.#request<AgentRosterEntry>({
476
506
  method: 'GET',
477
- path: `/v1/agents/roster/${encodeURIComponent(rosterId)}`,
507
+ path: `/agents/roster/${encodeURIComponent(rosterId)}`,
478
508
  });
479
509
  } catch (err) {
480
510
  if (err instanceof WopError && err.status === 404) return null;
@@ -482,26 +512,17 @@ export class OpenwopClient {
482
512
  }
483
513
  },
484
514
 
485
- /**
486
- * RFC 0087 §C — return the caller's agent org-chart (departments + roles +
487
- * `reportsTo` over roster members; descriptive — confers no authority).
488
- * Returns `null` when the host doesn't advertise `capabilities.agents.orgChart`.
489
- */
515
+ /** `GET /agents/org-chart` (RFC 0087 §C) — `null` when unadvertised. */
490
516
  getOrgChart: async (): Promise<AgentOrgChart | null> => {
491
517
  try {
492
- return await this.#request<AgentOrgChart>({ method: 'GET', path: '/v1/agents/org-chart' });
518
+ return await this.#request<AgentOrgChart>({ method: 'GET', path: '/agents/org-chart' });
493
519
  } catch (err) {
494
520
  if (err instanceof WopError && err.status === 404) return null;
495
521
  throw err;
496
522
  }
497
523
  },
498
524
 
499
- /**
500
- * RFC 0087 §D — one department's subtree + responsibility roll-up (the
501
- * union of its members' RFC 0086 portfolios). `recursive: false` scopes the
502
- * roll-up to direct members. Returns `null` on 404 (unknown/cross-tenant
503
- * department, or the capability is unadvertised).
504
- */
525
+ /** `GET /agents/org-chart/{departmentId}` (RFC 0087 §D) — `null` on `404`. */
505
526
  getOrgChartDepartment: async (
506
527
  departmentId: string,
507
528
  opts: { recursive?: boolean } = {},
@@ -510,7 +531,7 @@ export class OpenwopClient {
510
531
  try {
511
532
  return await this.#request<OrgChartResponsibilityView>({
512
533
  method: 'GET',
513
- path: `/v1/agents/org-chart/${encodeURIComponent(departmentId)}${qs}`,
534
+ path: `/agents/org-chart/${encodeURIComponent(departmentId)}${qs}`,
514
535
  });
515
536
  } catch (err) {
516
537
  if (err instanceof WopError && err.status === 404) return null;
@@ -519,35 +540,24 @@ export class OpenwopClient {
519
540
  },
520
541
  };
521
542
 
522
- // RFC 0078 — portable tool catalog (spec/v1/tool-catalog.md). The host
523
- // projects every node-pack / workflow / mcp / connector / host-extension
524
- // tool visible to the caller onto a uniform `ToolDescriptor`.
543
+ // ── RFC 0078 — portable tool catalog (gated on `toolCatalog`) ─────────
525
544
  readonly tools = {
526
- /**
527
- * RFC 0078 §B — list the portable `ToolDescriptor`s visible to the caller.
528
- * Returns `null` when the host doesn't advertise `capabilities.toolCatalog`
529
- * (the endpoint 404s), so callers can branch on capability discovery.
530
- */
545
+ /** `GET /tools` — `null` when the host doesn't advertise `toolCatalog`. */
531
546
  list: async (): Promise<readonly ToolDescriptor[] | null> => {
532
547
  try {
533
- return await this.#request<ToolDescriptor[]>({ method: 'GET', path: '/v1/tools' });
548
+ return await this.#request<ToolDescriptor[]>({ method: 'GET', path: '/tools' });
534
549
  } catch (err) {
535
550
  if (err instanceof WopError && err.status === 404) return null;
536
551
  throw err;
537
552
  }
538
553
  },
539
554
 
540
- /**
541
- * RFC 0112 — list the `CompactToolDescriptor`s via `GET /v1/tools?view=compact`
542
- * when the host advertises `capabilities.toolCatalog.compactView`. Unwraps the
543
- * `{ tools: CompactToolDescriptor[] }` envelope. Returns `null` when the host
544
- * doesn't advertise the catalog (the endpoint 404s).
545
- */
555
+ /** `GET /tools?view=compact` (RFC 0112) — unwraps `{ tools }`; `null` when unadvertised. */
546
556
  listCompact: async (): Promise<readonly CompactToolDescriptor[] | null> => {
547
557
  try {
548
558
  const res = await this.#request<{ tools?: readonly CompactToolDescriptor[] }>({
549
559
  method: 'GET',
550
- path: '/v1/tools?view=compact',
560
+ path: '/tools?view=compact',
551
561
  });
552
562
  return res.tools ?? [];
553
563
  } catch (err) {
@@ -556,12 +566,7 @@ export class OpenwopClient {
556
566
  }
557
567
  },
558
568
 
559
- /**
560
- * RFC 0078 §B — return one `ToolDescriptor` by its stable `toolId`. Returns
561
- * `null` on 404 (no such tool, or the capability is unadvertised). Pass
562
- * `{ view: 'compact' }` (RFC 0112) to receive the `CompactToolDescriptor`
563
- * projection instead.
564
- */
569
+ /** `GET /tools/{toolId}` — `null` on `404`; `{ view: 'compact' }` for the compact projection. */
565
570
  get: async (
566
571
  toolId: string,
567
572
  opts: { readonly view?: 'standard' | 'compact' } = {},
@@ -570,7 +575,7 @@ export class OpenwopClient {
570
575
  try {
571
576
  return await this.#request<ToolDescriptor | CompactToolDescriptor>({
572
577
  method: 'GET',
573
- path: `/v1/tools/${encodeURIComponent(toolId)}${query}`,
578
+ path: `/tools/${encodeURIComponent(toolId)}${query}`,
574
579
  });
575
580
  } catch (err) {
576
581
  if (err instanceof WopError && err.status === 404) return null;
@@ -579,61 +584,9 @@ export class OpenwopClient {
579
584
  },
580
585
  };
581
586
 
582
- // ── User-authored agents (sample-extension; non-normative) ───────────
583
- // Backs the workflow-engine sample app's Agents tab. Pack-installed
584
- // agents come through the `.agents` inventory above (RFC 0072 §A).
585
- // These methods wrap the `POST/DELETE /v1/host/sample/agents` +
586
- // `GET/POST /v1/host/sample/registry/agent-packs` host extensions.
587
- // Returns `null` on 404 (capability absent — matches the `.agents`
588
- // surface pattern); throws on other failures.
589
- readonly userAgents = {
590
- create: async (body: CreateUserAgentRequest, opts: MutationOptions = {}): Promise<UserAgentRecord> => {
591
- return await this.#request<UserAgentRecord>({
592
- method: 'POST',
593
- path: '/v1/host/sample/agents',
594
- body,
595
- ...(opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}),
596
- });
597
- },
598
-
599
- delete: async (agentId: string): Promise<boolean> => {
600
- try {
601
- await this.#request<void>({
602
- method: 'DELETE',
603
- path: `/v1/host/sample/agents/${encodeURIComponent(agentId)}`,
604
- });
605
- return true;
606
- } catch (err) {
607
- if (err instanceof WopError && err.status === 404) return false;
608
- throw err;
609
- }
610
- },
611
-
612
- listAvailablePacks: async (): Promise<AgentPackRegistryResponse | null> => {
613
- try {
614
- return await this.#request<AgentPackRegistryResponse>({
615
- method: 'GET',
616
- path: '/v1/host/sample/registry/agent-packs',
617
- });
618
- } catch (err) {
619
- if (err instanceof WopError && err.status === 404) return null;
620
- throw err;
621
- }
622
- },
623
-
624
- installPack: async (
625
- body: InstallAgentPackRequest,
626
- ): Promise<InstallAgentPackResponse> => {
627
- return await this.#request<InstallAgentPackResponse>({
628
- method: 'POST',
629
- path: '/v1/host/sample/registry/agent-packs/install',
630
- body,
631
- });
632
- },
633
- };
634
-
635
587
  // ── HITL interrupts (run-scoped + signed-token) ──────────────────────
636
588
  readonly interrupts = {
589
+ /** `POST /runs/{runId}/interrupts/{nodeId}` */
637
590
  resolveByRun: (
638
591
  runId: string,
639
592
  nodeId: string,
@@ -642,32 +595,26 @@ export class OpenwopClient {
642
595
  ): Promise<ResolveInterruptResponse> =>
643
596
  this.#request<ResolveInterruptResponse>({
644
597
  method: 'POST',
645
- path: `/v1/runs/${encodeURIComponent(runId)}/interrupts/${encodeURIComponent(nodeId)}`,
598
+ path: `/runs/${encodeURIComponent(runId)}/interrupts/${encodeURIComponent(nodeId)}`,
646
599
  body,
647
600
  headers: this.#mutationHeaders(opts),
648
601
  }),
649
602
 
650
603
  /**
651
- * Inspect an interrupt via signed token useful for showing the
652
- * interrupt's `kind`, `data`, and `resumeSchema` to a downstream
653
- * UI before the user resolves. Token is the auth, no API key
654
- * required (signed-token endpoints intentionally bypass bearer
655
- * auth so external systems can resolve without openwop credentials).
604
+ * `GET /interrupts/{token}` inspect via signed token. The token is the
605
+ * auth; no API key is sent (signed-token endpoints bypass bearer auth so
606
+ * external systems can resolve without openwop credentials).
656
607
  */
657
608
  inspectByToken: (token: string): Promise<InterruptByTokenInspection> =>
658
609
  this.#request<InterruptByTokenInspection>(
659
610
  {
660
611
  method: 'GET',
661
- path: `/v1/interrupts/${encodeURIComponent(token)}`,
612
+ path: `/interrupts/${encodeURIComponent(token)}`,
662
613
  },
663
614
  false, // unauthenticated (token IS the auth)
664
615
  ),
665
616
 
666
- /**
667
- * Resolve an interrupt via signed token — used by external
668
- * systems (calendar webhooks, payment confirmations) that the
669
- * engine handed a callback URL at suspension time.
670
- */
617
+ /** `POST /interrupts/{token}` — resolve via signed token. */
671
618
  resolveByToken: (
672
619
  token: string,
673
620
  body: ResolveInterruptRequest,
@@ -676,7 +623,7 @@ export class OpenwopClient {
676
623
  this.#request<ResolveInterruptByTokenResponse>(
677
624
  {
678
625
  method: 'POST',
679
- path: `/v1/interrupts/${encodeURIComponent(token)}`,
626
+ path: `/interrupts/${encodeURIComponent(token)}`,
680
627
  body,
681
628
  headers: this.#mutationHeaders(opts),
682
629
  },
@@ -684,14 +631,13 @@ export class OpenwopClient {
684
631
  ),
685
632
  };
686
633
 
687
- // ── Webhook subscriptions (per spec/v1/webhooks.md) ─────────────────────
634
+ // ── Webhook subscriptions (webhooks.md; gated on `webhooks`) ─────────
688
635
  readonly webhooks = {
689
636
  /**
690
- * Register a webhook subscription. Server signs deliveries with
691
- * HMAC-SHA256 over `${timestamp}.${rawBody}` using the
692
- * registration-time secret per `spec/v1/webhooks.md` §"Signature
693
- * recipe". The secret is returned ONCE in the response — store it
694
- * server-side for verification; the host cannot recover it.
637
+ * `POST /webhooks` `{ url, events[], secret?, tags? }`; `url` MUST be
638
+ * `https://`. Deliveries are signed HMAC-SHA256 over
639
+ * `${timestamp}.${rawBody}` under the `OpenWOP-*` header family (verify
640
+ * with `@openwop/openwop/webhooks`).
695
641
  */
696
642
  register: (
697
643
  body: RegisterWebhookRequest,
@@ -699,45 +645,23 @@ export class OpenwopClient {
699
645
  ): Promise<RegisterWebhookResponse> =>
700
646
  this.#request<RegisterWebhookResponse>({
701
647
  method: 'POST',
702
- path: '/v1/webhooks',
648
+ path: '/webhooks',
703
649
  body,
704
650
  headers: this.#mutationHeaders(opts),
705
651
  }),
706
652
 
707
- /**
708
- * Unregister a webhook subscription. Returns void on success;
709
- * throws `WopError` with `subscription_not_found` on unknown
710
- * subscriptionId.
711
- */
712
- unregister: async (subscriptionId: string): Promise<void> => {
653
+ /** `DELETE /webhooks/{webhookId}` — `204`; throws `404` when unknown, `403` outside the tenant. */
654
+ unregister: async (webhookId: string): Promise<void> => {
713
655
  await this.#request<unknown>({
714
656
  method: 'DELETE',
715
- path: `/v1/webhooks/${encodeURIComponent(subscriptionId)}`,
657
+ path: `/webhooks/${encodeURIComponent(webhookId)}`,
716
658
  });
717
659
  },
718
660
  };
719
661
 
720
- // ── Prompt library (RFC 0028; gated on capabilities.prompts.*) ──
721
- //
722
- // Read endpoints (list, get, render) gate on
723
- // `capabilities.prompts.endpointsSupported: true`. Mutating endpoints
724
- // (create, update, delete) additionally require
725
- // `capabilities.prompts.mutableLibrary: true`. Hosts that don't advertise
726
- // the relevant capability return `501 capability_not_provided`; the SDK
727
- // surfaces that as a `WopError`. Clients SHOULD pre-flight via
728
- // `getCapabilities()` before calling.
729
- //
730
- // NOTE: `capabilities.prompts.supported: true` (without
731
- // `endpointsSupported: true`) ONLY gates node-execution PromptRef
732
- // resolution per RFC 0027 Phase A; it does NOT imply these endpoints are
733
- // available. See spec/v1/prompts.md §"Capability advertisement" for the
734
- // two-axis gating split.
662
+ // ── Prompt library (RFC 0028; gated on `prompts`) ────────────────────
735
663
  readonly prompts = {
736
- /**
737
- * List prompt templates available to the caller per RFC 0028 §A
738
- * (operationId `listPromptTemplates`). Supports kind / tag / modelClass
739
- * / source filters + opaque cursor pagination.
740
- */
664
+ /** `GET /prompts` — kind / tag / modelClass / source filters + cursor pagination. */
741
665
  list: (req: ListPromptsRequest = {}): Promise<ListPromptsResponse> => {
742
666
  const search = new URLSearchParams();
743
667
  if (req.kind) search.set('kind', req.kind);
@@ -749,16 +673,11 @@ export class OpenwopClient {
749
673
  const query = search.toString();
750
674
  return this.#request<ListPromptsResponse>({
751
675
  method: 'GET',
752
- path: `/v1/prompts${query ? `?${query}` : ''}`,
676
+ path: `/prompts${query ? `?${query}` : ''}`,
753
677
  });
754
678
  },
755
679
 
756
- /**
757
- * Fetch a single PromptTemplate by id per RFC 0028 §A
758
- * (operationId `getPromptTemplate`). Optionally pin a SemVer
759
- * `version`; supply `libraryId` to disambiguate when multiple installed
760
- * packs ship the same templateId.
761
- */
680
+ /** `GET /prompts/{templateId}` — `null` on `404`; a `400 prompt_ref_ambiguous` still throws. */
762
681
  get: async (req: GetPromptRequest): Promise<PromptTemplate | null> => {
763
682
  const search = new URLSearchParams();
764
683
  if (req.version) search.set('version', req.version);
@@ -767,58 +686,34 @@ export class OpenwopClient {
767
686
  try {
768
687
  return await this.#request<PromptTemplate>({
769
688
  method: 'GET',
770
- path: `/v1/prompts/${encodeURIComponent(req.templateId)}${query ? `?${query}` : ''}`,
689
+ path: `/prompts/${encodeURIComponent(req.templateId)}${query ? `?${query}` : ''}`,
771
690
  });
772
691
  } catch (err) {
773
- // Return null on 404 (no such template), consistent with the other
774
- // get-by-id methods (`agents.get`, `tools.get`, …) and the Python/Go
775
- // SDKs; a `400 prompt_ref_ambiguous` and other errors still throw so
776
- // callers can distinguish "not found" from "ambiguous reference".
777
692
  if (err instanceof WopError && err.status === 404) return null;
778
693
  throw err;
779
694
  }
780
695
  },
781
696
 
782
- /**
783
- * Render a PromptTemplate with supplied variable bindings per RFC 0028
784
- * §A (operationId `renderPromptTemplate`). Returns composed body +
785
- * sha256 hash + per-variable hashes. The deterministic-hash invariant
786
- * (RFC 0028 §A) requires the `hash` to match what a matching
787
- * `prompt.composed` event would carry at dispatch time. Does NOT
788
- * dispatch an LLM call. Secret-source variable values MUST be supplied
789
- * as `[REDACTED:<credentialRef>]` markers per
790
- * SECURITY/threat-model-secret-leakage.md §SR-1.
791
- */
697
+ /** `POST /prompts:render` — composed body + sha256 hash; does NOT dispatch an LLM call. */
792
698
  render: (req: RenderPromptRequest): Promise<RenderPromptResponse> => {
793
699
  return this.#request<RenderPromptResponse>({
794
700
  method: 'POST',
795
- path: '/v1/prompts:render',
701
+ path: '/prompts:render',
796
702
  body: req,
797
703
  });
798
704
  },
799
705
 
800
- /**
801
- * Create a new user-source PromptTemplate per RFC 0028 §A
802
- * (operationId `createPromptTemplate`). Mutating endpoint —
803
- * requires `capabilities.prompts.mutableLibrary: true`. Supports
804
- * `Idempotency-Key` per the standard `MutationOptions` pattern.
805
- */
706
+ /** `POST /prompts` — requires `prompts.mutableLibrary`. */
806
707
  create: (template: PromptTemplate, opts: MutationOptions = {}): Promise<void> => {
807
708
  return this.#request<void>({
808
709
  method: 'POST',
809
- path: '/v1/prompts',
710
+ path: '/prompts',
810
711
  body: template,
811
712
  headers: this.#mutationHeaders(opts),
812
713
  });
813
714
  },
814
715
 
815
- /**
816
- * Replace an existing user-source PromptTemplate per RFC 0028 §A
817
- * (operationId `updatePromptTemplate`). Submitted SemVer MUST be
818
- * strictly greater than stored. Mutating endpoint — requires
819
- * `capabilities.prompts.mutableLibrary: true`. Pack-sourced and
820
- * host-built-in templates are read-only (host returns 403).
821
- */
716
+ /** `PUT /prompts/{templateId}` — the submitted SemVer MUST be strictly greater than stored. */
822
717
  update: (
823
718
  templateId: string,
824
719
  template: PromptTemplate,
@@ -826,38 +721,27 @@ export class OpenwopClient {
826
721
  ): Promise<PromptTemplate> => {
827
722
  return this.#request<PromptTemplate>({
828
723
  method: 'PUT',
829
- path: `/v1/prompts/${encodeURIComponent(templateId)}`,
724
+ path: `/prompts/${encodeURIComponent(templateId)}`,
830
725
  body: template,
831
726
  headers: this.#mutationHeaders(opts),
832
727
  });
833
728
  },
834
729
 
835
- /**
836
- * Delete a user-source PromptTemplate per RFC 0028 §A
837
- * (operationId `deletePromptTemplate`). Mutating endpoint —
838
- * requires `capabilities.prompts.mutableLibrary: true`. Pack-sourced
839
- * and host-built-in templates are read-only (host returns 403).
840
- */
730
+ /** `DELETE /prompts/{templateId}` */
841
731
  delete: (templateId: string): Promise<void> => {
842
732
  return this.#request<void>({
843
733
  method: 'DELETE',
844
- path: `/v1/prompts/${encodeURIComponent(templateId)}`,
734
+ path: `/prompts/${encodeURIComponent(templateId)}`,
845
735
  });
846
736
  },
847
737
  };
848
738
 
849
- // ── Audit-log integrity (gated on openwop-audit-log-integrity profile) ──
739
+ // ── Audit-log integrity ──────────────────────────────────────────────
850
740
  readonly audit = {
851
741
  /**
852
- * Verify the audit-log hash chain over `[fromSeq, toSeq]` per
853
- * `auth-profiles.md` §`openwop-audit-log-integrity` §4. Requires
854
- * the `audit:read` scope on the API key. Returns chain-validity
855
- * verdict + signed checkpoints + any detected anomalies.
856
- *
857
- * Hosts that do NOT advertise the profile return `404` and throw
858
- * a `WopError`. Clients SHOULD pre-flight via
859
- * `client.getCapabilities()` (or directly inspect
860
- * `capabilities.auth.profiles[]`) before calling.
742
+ * `GET /audit/verify?fromSeq&toSeq` chain-validity verdict + signed
743
+ * checkpoints + anomalies. Requires the `audit:read` scope; a host that
744
+ * does not serve the profile answers `404`.
861
745
  */
862
746
  verify: (fromSeq: number, toSeq: number): Promise<AuditVerifyResult> => {
863
747
  const search = new URLSearchParams();
@@ -865,20 +749,19 @@ export class OpenwopClient {
865
749
  search.set('toSeq', String(toSeq));
866
750
  return this.#request<AuditVerifyResult>({
867
751
  method: 'GET',
868
- path: `/v1/audit/verify?${search.toString()}`,
752
+ path: `/audit/verify?${search.toString()}`,
869
753
  });
870
754
  },
871
755
  };
872
756
 
873
- // ── RFC 0103 Localized content surface (gated on capabilities.content) ──
757
+ // ── RFC 0103 Localized content surface (gated on `content`) ──────────
874
758
  readonly content = {
875
- /** `GET /v1/content/pages` — list page records. Returns `null` when the
876
- * host doesn't advertise `capabilities.content` (501). */
759
+ /** `GET /content/pages` — `null` when the host doesn't advertise `content` (501). */
877
760
  listPages: async (): Promise<readonly LocalizedContentPage[] | null> => {
878
761
  try {
879
762
  return await this.#request<readonly LocalizedContentPage[]>({
880
763
  method: 'GET',
881
- path: '/v1/content/pages',
764
+ path: '/content/pages',
882
765
  });
883
766
  } catch (err) {
884
767
  if (err instanceof WopError && err.status === 501) return null;
@@ -886,10 +769,7 @@ export class OpenwopClient {
886
769
  }
887
770
  },
888
771
 
889
- /** `GET /v1/content/pages/{slug}` — the negotiated locale's resolved page +
890
- * sections. `acceptLanguage` rides the `Accept-Language` header (the Stable
891
- * `i18n.md` negotiation; no `?locale=`). Returns `null` on `404`
892
- * (no such published page) or `501` (uncapable). */
772
+ /** `GET /content/pages/{slug}` — `acceptLanguage` rides `Accept-Language`; `null` on `404`/`501`. */
893
773
  getPage: async (
894
774
  slug: string,
895
775
  acceptLanguage?: string,
@@ -897,7 +777,7 @@ export class OpenwopClient {
897
777
  try {
898
778
  return await this.#request<LocalizedContentPageResponse>({
899
779
  method: 'GET',
900
- path: `/v1/content/pages/${encodeURIComponent(slug)}`,
780
+ path: `/content/pages/${encodeURIComponent(slug)}`,
901
781
  ...(acceptLanguage
902
782
  ? { headers: { 'Accept-Language': acceptLanguage } }
903
783
  : {}),
@@ -909,17 +789,15 @@ export class OpenwopClient {
909
789
  }
910
790
  },
911
791
 
912
- /** `POST /v1/content/pages` — create a page record (admin). Throws the
913
- * typed `WopError` on `400`/`401`/`403`. */
792
+ /** `POST /content/pages` (admin). */
914
793
  createPage: (body: LocalizedContentPage): Promise<LocalizedContentPage> =>
915
794
  this.#request<LocalizedContentPage>({
916
795
  method: 'POST',
917
- path: '/v1/content/pages',
796
+ path: '/content/pages',
918
797
  body,
919
798
  }),
920
799
 
921
- /** `PUT /v1/content/pages/{pageId}/sections/{sectionId}` upsert a
922
- * section's field overlay for a locale (admin). */
800
+ /** `PUT /content/pages/{pageId}/sections/{sectionId}` (admin). */
923
801
  putSection: (
924
802
  pageId: string,
925
803
  sectionId: string,
@@ -927,17 +805,16 @@ export class OpenwopClient {
927
805
  ): Promise<LocalizedContentSection> =>
928
806
  this.#request<LocalizedContentSection>({
929
807
  method: 'PUT',
930
- path: `/v1/content/pages/${encodeURIComponent(pageId)}/sections/${encodeURIComponent(sectionId)}`,
808
+ path: `/content/pages/${encodeURIComponent(pageId)}/sections/${encodeURIComponent(sectionId)}`,
931
809
  body,
932
810
  }),
933
811
 
934
- /** `GET /v1/content/settings` — language settings. Returns `null` when the
935
- * host doesn't advertise `capabilities.content` (501). */
812
+ /** `GET /content/settings` — `null` when the host doesn't advertise `content` (501). */
936
813
  getSettings: async (): Promise<LocalizedContentLanguageSettings | null> => {
937
814
  try {
938
815
  return await this.#request<LocalizedContentLanguageSettings>({
939
816
  method: 'GET',
940
- path: '/v1/content/settings',
817
+ path: '/content/settings',
941
818
  });
942
819
  } catch (err) {
943
820
  if (err instanceof WopError && err.status === 501) return null;
@@ -945,126 +822,44 @@ export class OpenwopClient {
945
822
  }
946
823
  },
947
824
 
948
- /** `PUT /v1/content/settings` — replace language settings (admin). */
825
+ /** `PUT /content/settings` (admin). */
949
826
  putSettings: (
950
827
  body: LocalizedContentLanguageSettings,
951
828
  ): Promise<LocalizedContentLanguageSettings> =>
952
829
  this.#request<LocalizedContentLanguageSettings>({
953
830
  method: 'PUT',
954
- path: '/v1/content/settings',
831
+ path: '/content/settings',
955
832
  body,
956
833
  }),
957
834
  };
958
835
 
959
- // ── RFC 0099 Trigger subscriptions (gated on capabilities.triggerBridge) ──
836
+ // ── RFC 0099 Trigger subscriptions (gated on `triggerBridge`) ─────────
960
837
  readonly triggerSubscriptions = {
961
- /** `POST /v1/trigger-subscriptions` — register an external-event trigger.
962
- * The `binding.secret*` is returned ONCE at creation (SR-1); persist it.
963
- * Throws the typed `WopError` on `400`/`401`/`403`, or `501` when the host
964
- * doesn't advertise the trigger-bridge ingestion surface. */
838
+ /** `POST /trigger-subscriptions` — the `binding.secret*` is returned ONCE; persist it. */
965
839
  create: (
966
840
  body: TriggerSubscriptionRegistration,
967
841
  ): Promise<CreateTriggerSubscriptionResponse> =>
968
842
  this.#request<CreateTriggerSubscriptionResponse>({
969
843
  method: 'POST',
970
- path: '/v1/trigger-subscriptions',
844
+ path: '/trigger-subscriptions',
971
845
  body,
972
846
  }),
973
847
  };
974
848
 
975
- // ── Agent workspace files (RFC 0059; gated on capabilities.workspace) ──
976
- readonly workspace = {
977
- /**
978
- * RFC 0059 — list workspace file metadata (no bodies) for the caller's
979
- * `{tenant, workspace}`. Optional `prefix` filters the flat `path`
980
- * namespace. Returns `null` when the host doesn't advertise
981
- * `capabilities.workspace.supported` (501), so callers can branch on
982
- * capability discovery without try/catch.
983
- */
984
- listFiles: async (opts: { prefix?: string } = {}): Promise<readonly WorkspaceFile[] | null> => {
985
- const search = new URLSearchParams();
986
- if (opts.prefix !== undefined) search.set('prefix', opts.prefix);
987
- const qs = search.toString();
988
- try {
989
- const res = await this.#request<{ files: WorkspaceFile[] }>({
990
- method: 'GET',
991
- path: `/v1/host/workspace/files${qs ? `?${qs}` : ''}`,
992
- });
993
- return res.files;
994
- } catch (err) {
995
- if (err instanceof WopError && err.status === 501) return null;
996
- throw err;
997
- }
998
- },
999
-
1000
- /**
1001
- * RFC 0059 — read one workspace file. Pass `version` for a historical
1002
- * snapshot when `capabilities.workspace.versioned`. Returns `null` when
1003
- * the file is absent (404) or the host doesn't advertise the capability
1004
- * (501).
1005
- */
1006
- getFile: async (path: string, opts: { version?: number } = {}): Promise<WorkspaceFile | null> => {
1007
- const search = new URLSearchParams();
1008
- if (opts.version !== undefined) search.set('version', String(opts.version));
1009
- const qs = search.toString();
1010
- try {
1011
- return await this.#request<WorkspaceFile>({
1012
- method: 'GET',
1013
- path: `/v1/host/workspace/files/${encodeURIComponent(path)}${qs ? `?${qs}` : ''}`,
1014
- });
1015
- } catch (err) {
1016
- if (err instanceof WopError && (err.status === 404 || err.status === 501)) return null;
1017
- throw err;
1018
- }
1019
- },
1020
-
1021
- /**
1022
- * RFC 0059 — atomic create/replace of a workspace file. Pass `ifMatch`
1023
- * (the file's current `etag`) for optimistic concurrency; a stale token
1024
- * throws a `WopError` with status `409` (`workspace_conflict`). Content
1025
- * beyond `capabilities.workspace.maxFileBytes` throws `413`
1026
- * (`workspace_too_large`). Returns the persisted `WorkspaceFile`.
1027
- */
1028
- putFile: (
1029
- path: string,
1030
- body: PutWorkspaceFileRequest,
1031
- opts: MutationOptions & { ifMatch?: string } = {},
1032
- ): Promise<WorkspaceFile> => {
1033
- const headers = this.#mutationHeaders(opts);
1034
- if (opts.ifMatch !== undefined) headers['If-Match'] = opts.ifMatch;
1035
- return this.#request<WorkspaceFile>({
1036
- method: 'PUT',
1037
- path: `/v1/host/workspace/files/${encodeURIComponent(path)}`,
1038
- body,
1039
- headers,
1040
- });
1041
- },
1042
-
1043
- /**
1044
- * RFC 0059 — delete a workspace file. Returns `true` on success (`204`),
1045
- * `false` when the file is absent (404) or the host doesn't advertise the
1046
- * capability (501).
1047
- */
1048
- deleteFile: async (path: string, opts: MutationOptions = {}): Promise<boolean> => {
1049
- try {
1050
- await this.#request<void>({
1051
- method: 'DELETE',
1052
- path: `/v1/host/workspace/files/${encodeURIComponent(path)}`,
1053
- headers: this.#mutationHeaders(opts),
1054
- });
1055
- return true;
1056
- } catch (err) {
1057
- if (err instanceof WopError && (err.status === 404 || err.status === 501)) return false;
1058
- throw err;
1059
- }
1060
- },
1061
- };
1062
-
1063
849
  // ── Internals ────────────────────────────────────────────────────────
850
+ #streamContext(): { baseUrl: string; apiKey: string; protocolVersion: string; fetch: typeof fetch } {
851
+ return {
852
+ baseUrl: this.#baseUrl,
853
+ apiKey: this.#apiKey,
854
+ protocolVersion: this.#versionHeader,
855
+ fetch: this.#fetch,
856
+ };
857
+ }
858
+
1064
859
  #mutationHeaders(opts: MutationOptions): Record<string, string> {
1065
860
  const h: Record<string, string> = {};
1066
861
  if (opts.idempotencyKey) h['Idempotency-Key'] = opts.idempotencyKey;
1067
- if (opts.dedup) h['X-Dedup'] = opts.dedup;
862
+ if (opts.dedup) h['OpenWOP-Dedup'] = opts.dedup;
1068
863
  return h;
1069
864
  }
1070
865
 
@@ -1072,6 +867,8 @@ export class OpenwopClient {
1072
867
  const url = `${this.#baseUrl}${opts.path}`;
1073
868
  const headers: Record<string, string> = {
1074
869
  Accept: 'application/json',
870
+ // RFC 0172 §A.3 — on every request, authenticated or not.
871
+ 'OpenWOP-Version': this.#versionHeader,
1075
872
  ...(opts.headers ?? {}),
1076
873
  };
1077
874
  if (opts.body !== undefined && headers['Content-Type'] === undefined) {
@@ -1080,7 +877,7 @@ export class OpenwopClient {
1080
877
  if (authenticated) {
1081
878
  headers.Authorization = `Bearer ${this.#apiKey}`;
1082
879
  }
1083
- if (this.#acceptLanguage) {
880
+ if (this.#acceptLanguage && headers['Accept-Language'] === undefined) {
1084
881
  headers['Accept-Language'] = this.#acceptLanguage;
1085
882
  }
1086
883
 
@@ -1094,19 +891,16 @@ export class OpenwopClient {
1094
891
 
1095
892
  const res = await this.#fetch(url, init);
1096
893
  const text = await res.text();
1097
- // Capture traceparent for error reporting per observability.md
1098
- // §Trace context propagation. Header name is case-insensitive per
1099
- // RFC 9110; fetch normalizes to lowercase but be defensive.
894
+ // Capture traceparent for error reporting (observability.md §Trace
895
+ // context propagation). Header names are case-insensitive per RFC 9110.
1100
896
  const traceparent =
1101
897
  res.headers.get('traceparent') ?? res.headers.get('Traceparent') ?? undefined;
1102
898
 
1103
899
  if (!res.ok) {
1104
900
  let env: ErrorEnvelope | undefined;
1105
901
  try {
1106
- const parsed = text.length > 0 ? JSON.parse(text) : undefined;
1107
- if (parsed && typeof parsed === 'object' && 'error' in parsed && 'message' in parsed) {
1108
- env = parsed as ErrorEnvelope;
1109
- }
902
+ const parsed: unknown = text.length > 0 ? JSON.parse(text) : undefined;
903
+ if (isErrorEnvelope(parsed)) env = parsed;
1110
904
  } catch {
1111
905
  // not JSON; leave envelope undefined
1112
906
  }
@@ -1121,11 +915,18 @@ export class OpenwopClient {
1121
915
  res.status,
1122
916
  text,
1123
917
  {
1124
- error: 'invalid_json',
918
+ error: 'internal_error',
1125
919
  message: 'Server returned non-JSON body for a 2xx response',
920
+ details: { sdk: 'invalid_json' },
1126
921
  },
1127
922
  traceparent,
1128
923
  );
1129
924
  }
1130
925
  }
1131
926
  }
927
+
928
+ function isErrorEnvelope(value: unknown): value is ErrorEnvelope {
929
+ if (typeof value !== 'object' || value === null) return false;
930
+ const rec = value as Record<string, unknown>;
931
+ return typeof rec['error'] === 'string' && typeof rec['message'] === 'string';
932
+ }