@openwop/openwop 1.9.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 -18
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +26 -56
  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 +22 -13
  32. package/dist/webhook-header-families.d.ts.map +1 -1
  33. package/dist/webhook-header-families.js +30 -27
  34. package/dist/webhook-header-families.js.map +1 -1
  35. package/dist/webhook-helpers.browser.d.ts +15 -29
  36. package/dist/webhook-helpers.browser.d.ts.map +1 -1
  37. package/dist/webhook-helpers.browser.js +16 -31
  38. package/dist/webhook-helpers.browser.js.map +1 -1
  39. package/dist/webhook-helpers.d.ts +41 -41
  40. package/dist/webhook-helpers.d.ts.map +1 -1
  41. package/dist/webhook-helpers.js +40 -47
  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 -110
  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 +41 -29
  54. package/src/webhook-helpers.browser.ts +23 -32
  55. package/src/webhook-helpers.ts +67 -53
  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
@@ -4,7 +4,7 @@
4
4
  * declared in `types.ts`.
5
5
  *
6
6
  * **Why these live here.** `types.ts` declares the canonical wire shapes
7
- * (mirroring `api/openapi.yaml` + `schemas/run-snapshot.schema.json`).
7
+ * (mirroring `api/v2/openapi.yaml` + `schemas/v2/run-snapshot.schema.json`).
8
8
  * This module adds the constants + predicates SDK consumers need to act
9
9
  * on those shapes without redefining them locally. Pulling them into the
10
10
  * SDK makes the protocol vocabulary single-sourced for application,
@@ -28,7 +28,8 @@
28
28
  * @module @openwop/openwop/run-helpers
29
29
  */
30
30
 
31
- import type { RunStatus } from './types.js';
31
+ import type { RunStatus, VendorErrorCode } from './types.js';
32
+ import { ERROR_CODES, RETRIABLE_ERROR_CODES, VENDOR_ERROR_CODE_PATTERN, type ErrorCode } from './generated.js';
32
33
 
33
34
  // ─── Run statuses ────────────────────────────────────────────────────────
34
35
 
@@ -85,92 +86,31 @@ export function isTerminalRunStatus(status: RunStatus | string): boolean {
85
86
  // ─── HTTP error-envelope codes ───────────────────────────────────────────
86
87
 
87
88
  /**
88
- * Canonical REST/MCP error-envelope codes from `auth.md`,
89
- * `rest-endpoints.md`, and adjacent v1 specs. The HTTP envelope's `error`
90
- * field remains string-typed for forward compatibility, but this list
91
- * gives SDK consumers a stable set for common branching.
92
- *
93
- * Codes here describe request/transport failures. Run execution failures
94
- * live in `RUN_ERROR_CODES` below as `RunSnapshot.error.code`.
89
+ * The v2 error registry (`spec/v2/errors.json`, errors.md): every code a v2
90
+ * host may return in `ErrorEnvelope.error`, generated into `generated.ts`.
91
+ * Kept under the 1.x name for call-site parity; `ERROR_CODES` is the same
92
+ * array.
95
93
  */
96
- export const HTTP_ERROR_CODES = [
97
- // Auth / access
98
- 'unauthenticated',
99
- 'forbidden',
100
- 'key_expired',
101
- 'key_revoked',
102
-
103
- // Request / routing
104
- 'validation_error',
105
- 'not_found',
106
- 'rate_limited',
107
-
108
- // Idempotency / run creation conflicts
109
- 'run_already_active',
110
- 'idempotency_in_flight',
111
- 'idempotency_key_mismatch',
94
+ export const HTTP_ERROR_CODES = ERROR_CODES;
112
95
 
113
- // Streaming / protocol negotiation
114
- 'unsupported_stream_mode',
115
- 'force_engine_version_forbidden',
116
- 'mock_provider_forbidden',
96
+ export type HttpErrorCode = ErrorCode;
117
97
 
118
- // Capability / credential negotiation
119
- 'capability_not_provided',
120
- 'capability_required',
121
- 'credential_required',
122
- 'credential_forbidden',
123
- 'credential_unavailable',
124
-
125
- // Node-pack lifecycle (registry + lockfile) per node-packs.md §"Dependency resolution + lockfile"
126
- 'pack_integrity_mismatch',
127
- 'pack_signature_invalid',
128
- 'pack_peer_dependency_missing',
129
- 'pack_lockfile_incomplete',
130
- 'pack_version_not_found',
131
-
132
- // HITL / interrupt callbacks
133
- 'interrupt_not_found',
134
- 'approval_token_invalid',
135
- 'approval_token_expired',
136
- 'approval_token_consumed',
137
-
138
- // Phase H.1″ — AI provider policy enforcement per
139
- // capabilities.md §"aiProviders.policies".
140
- 'provider_policy_denied',
141
-
142
- // Phase H.2 — MCP client error codes.
143
- 'mcp_server_not_configured',
144
- 'mcp_timeout',
145
- 'mcp_network_error',
146
- 'mcp_server_error',
147
- 'mcp_protocol_error',
148
- 'mcp_tool_error',
149
-
150
- // Phase H.3 — HTTP client error codes.
151
- 'http_url_rejected',
152
- 'http_timeout',
153
- 'http_network_error',
154
- 'http_unexpected_status',
155
-
156
- // Phase H webhook codes (restored to spec-de-facto per Python host
157
- // close-out and conformance webhook-negative.test.ts).
158
- 'webhook_url_rejected',
159
- 'subscription_not_found',
98
+ /** Type guard that narrows a string to a registered protocol {@link ErrorCode}. */
99
+ export function isErrorCode(value: unknown): value is ErrorCode {
100
+ return typeof value === 'string' && (ERROR_CODES as readonly string[]).includes(value);
101
+ }
160
102
 
161
- // Generic server failure
162
- 'internal_error',
163
- ] as const;
103
+ /** 1.x name for {@link isErrorCode}. */
104
+ export const isHttpErrorCode = isErrorCode;
164
105
 
165
- export type HttpErrorCode = (typeof HTTP_ERROR_CODES)[number];
106
+ /** True for the registry rows marked `retriable: true`; retry timing lives in `Retry-After` only (RFC 0171 §B.2). */
107
+ export function isRetriableErrorCode(value: unknown): value is (typeof RETRIABLE_ERROR_CODES)[number] {
108
+ return typeof value === 'string' && (RETRIABLE_ERROR_CODES as readonly string[]).includes(value);
109
+ }
166
110
 
167
- /**
168
- * Type guard that narrows a string to a known canonical HTTP error code.
169
- * Returns false for host extensions and future spec additions; callers
170
- * should still render a fallback from the envelope's `message`.
171
- */
172
- export function isHttpErrorCode(value: unknown): value is HttpErrorCode {
173
- return typeof value === 'string' && (HTTP_ERROR_CODES as readonly string[]).includes(value);
111
+ /** True for a well-formed vendor code (`<org>.<name>`, `openwop.` reserved); does not check org registration. */
112
+ export function isVendorErrorCode(value: unknown): value is VendorErrorCode {
113
+ return typeof value === 'string' && VENDOR_ERROR_CODE_PATTERN.test(value);
174
114
  }
175
115
 
176
116
  // ─── Run error codes ─────────────────────────────────────────────────────
@@ -221,6 +161,9 @@ export const RUN_ERROR_CODES = [
221
161
  'node_execution_failed',
222
162
  'external_call_failed',
223
163
  'recursion_limit_exceeded',
164
+ 'run_timeout',
165
+ 'loop_limit_exceeded',
166
+ 'envelope_refusal',
224
167
  'capability_not_provided',
225
168
 
226
169
  // Approval
@@ -253,9 +196,8 @@ export function isRunErrorCode(value: unknown): value is RunErrorCode {
253
196
  * - `RunError` lives on the run document; describes WHY the run failed.
254
197
  * `code` is from the typed {@link RunErrorCode} vocabulary.
255
198
  * - `ErrorEnvelope` lives on HTTP error responses; describes WHY the
256
- * request failed. `error` is a free string code (often from
257
- * {@link HTTP_ERROR_CODES}, but not type-pinned, to permit host
258
- * extensions and future protocol additions).
199
+ * request failed. `error` is a registered {@link ErrorCode} or a vendor
200
+ * code (`<org>.<name>`).
259
201
  *
260
202
  * The shapes share `message` and `details` but diverge on the code field
261
203
  * name (`code` vs `error`) by design — they represent different layers.
package/src/sse.ts CHANGED
@@ -1,41 +1,58 @@
1
1
  /**
2
- * SSE consumer for `GET /v1/runs/{runId}/events`. Async-iterable shape so
3
- * consumers can write `for await (const event of client.runs.events(...))`.
2
+ * SSE consumers for `GET /runs/{runId}/events` (the `runEvents` channel) and
3
+ * `GET /host/events` (the `hostEvents` channel) events.md §SSE frames.
4
+ * Async-iterable shape so consumers can write
5
+ * `for await (const event of client.runs.events(...))`.
4
6
  *
5
- * Implementation parses event:/data:/id: lines per RFC 8895. Native fetch +
6
- * ReadableStream — zero third-party deps.
7
+ * Parses `event:` / `data:` / `id:` lines per the WHATWG EventSource
8
+ * grammar. Native fetch + ReadableStream — zero third-party deps.
7
9
  *
8
- * Designed to be cancellable: pass an AbortSignal via options, or break out
9
- * of the for-await loop and the underlying connection is torn down on the
10
- * next tick.
10
+ * Cancellable: pass an AbortSignal via options, or break out of the
11
+ * for-await loop and the underlying connection is torn down.
11
12
  */
12
13
 
13
- import type { RunEventDoc, StreamMode } from './types.js';
14
+ import type { HostEventDoc, RunEventDoc, StreamMode } from './types.js';
14
15
 
15
16
  export interface EventsStreamOptions {
16
17
  /**
17
- * Single mode (e.g., 'updates') OR array of modes (S4 mixed-mode,
18
- * e.g., ['updates', 'messages']). Arrays serialize to a comma-
19
- * separated `?streamMode=updates,messages` query.
18
+ * Single mode (e.g., 'updates') OR array of modes (mixed mode, e.g.,
19
+ * ['updates', 'messages']). Arrays serialize to a comma-separated
20
+ * `?streamMode=updates,messages` query (events.md §The events channel).
20
21
  */
21
22
  readonly streamMode?: StreamMode | readonly StreamMode[];
23
+ /** `Last-Event-ID` — the host resumes at the next sequence and never re-emits the resumption point. */
22
24
  readonly lastEventId?: string;
23
25
  readonly signal?: AbortSignal;
24
26
  /**
25
- * S3 batching hint. When set, server batches events for up to N ms;
26
- * the SDK transparently flattens batched arrays back into individual
27
- * RunEventDoc yields, so consumers see the same per-event surface as
28
- * unbuffered streams. Range 0..5000.
27
+ * Batching hint (0..5000). The host accumulates events into one
28
+ * `event: batch` frame whose `data:` is an array of `RunEventDoc`; the SDK
29
+ * flattens the batch back into individual yields.
29
30
  */
30
31
  readonly bufferMs?: number;
31
32
  }
32
33
 
34
+ export interface HostEventsStreamOptions {
35
+ /** `Last-Event-ID` for resumption. */
36
+ readonly lastEventId?: string;
37
+ readonly signal?: AbortSignal;
38
+ /**
39
+ * The channel address. Default `/host/events`; a host MAY declare another
40
+ * under `heartbeat.deliveryChannel` (capabilities.md).
41
+ */
42
+ readonly path?: string;
43
+ }
44
+
33
45
  export interface EventsStreamContext {
34
46
  readonly baseUrl: string;
35
47
  readonly apiKey: string;
48
+ /** The `OpenWOP-Version` value sent on the subscribe request (RFC 0172 §A.3). */
49
+ readonly protocolVersion: string;
50
+ /** Optional fetch implementation override. Defaults to global `fetch`. */
51
+ readonly fetch?: typeof fetch;
36
52
  }
37
53
 
38
- export async function* streamEvents(
54
+ /** Subscribe to a run's event stream. Yields each `RunEventDoc` until the host closes after the terminal event. */
55
+ export function streamEvents(
39
56
  ctx: EventsStreamContext,
40
57
  runId: string,
41
58
  opts: EventsStreamOptions = {},
@@ -50,12 +67,30 @@ export async function* streamEvents(
50
67
  params.set('bufferMs', String(opts.bufferMs));
51
68
  }
52
69
  const qs = params.toString();
53
- const url = `${ctx.baseUrl}/v1/runs/${encodeURIComponent(runId)}/events${qs ? `?${qs}` : ''}`;
70
+ const path = `/runs/${encodeURIComponent(runId)}/events${qs ? `?${qs}` : ''}`;
71
+ return streamSse<RunEventDoc>(ctx, path, opts);
72
+ }
73
+
74
+ /** Subscribe to the host events channel (heartbeat messages; content-free of run data). */
75
+ export function streamHostEvents(
76
+ ctx: EventsStreamContext,
77
+ opts: HostEventsStreamOptions = {},
78
+ ): AsyncGenerator<HostEventDoc, void, void> {
79
+ return streamSse<HostEventDoc>(ctx, opts.path ?? '/host/events', opts);
80
+ }
81
+
82
+ async function* streamSse<T>(
83
+ ctx: EventsStreamContext,
84
+ path: string,
85
+ opts: { readonly lastEventId?: string; readonly signal?: AbortSignal },
86
+ ): AsyncGenerator<T, void, void> {
87
+ const url = `${ctx.baseUrl}${path}`;
54
88
 
55
89
  const headers: Record<string, string> = {
56
90
  Accept: 'text/event-stream',
57
91
  Authorization: `Bearer ${ctx.apiKey}`,
58
92
  'Cache-Control': 'no-cache',
93
+ 'OpenWOP-Version': ctx.protocolVersion,
59
94
  };
60
95
  if (opts.lastEventId) {
61
96
  headers['Last-Event-ID'] = opts.lastEventId;
@@ -68,7 +103,8 @@ export async function* streamEvents(
68
103
  else externalSignal.addEventListener('abort', () => internalAbort.abort(), { once: true });
69
104
  }
70
105
 
71
- const res = await fetch(url, { method: 'GET', headers, signal: internalAbort.signal });
106
+ const doFetch = ctx.fetch ?? fetch;
107
+ const res = await doFetch(url, { method: 'GET', headers, signal: internalAbort.signal });
72
108
  if (!res.ok || res.body === null) {
73
109
  throw new Error(`SSE subscribe failed: HTTP ${res.status}`);
74
110
  }
@@ -78,36 +114,29 @@ export async function* streamEvents(
78
114
  let buffer = '';
79
115
  let pendingEvent = 'message';
80
116
  let pendingData: string[] = [];
81
- let pendingId: string | null = null;
82
117
 
83
118
  /**
84
- * Flush the buffered event. Returns an array of RunEventDoc:
85
- * - 0 elements when the buffer is empty or non-JSON (skip).
86
- * - 1 element for a normal `event: <type>` event.
87
- * - N elements when the server batched per S3 — `event: batch` with
88
- * `data:` as a JSON array of RunEventDoc.
119
+ * Flush the buffered frame. Returns 0 elements when the buffer is empty or
120
+ * non-JSON (skip), 1 element for a normal `event: <type>` frame, N for an
121
+ * `event: batch` frame whose `data:` is a JSON array.
89
122
  */
90
- const flushAndYield = (): RunEventDoc[] => {
123
+ const flushAndYield = (): T[] => {
91
124
  if (pendingData.length === 0) {
92
125
  pendingEvent = 'message';
93
- pendingId = null;
94
126
  return [];
95
127
  }
96
128
  const dataStr = pendingData.join('\n');
97
129
  const eventType = pendingEvent;
98
130
  pendingEvent = 'message';
99
131
  pendingData = [];
100
- pendingId = null;
101
132
  try {
102
133
  const parsed = JSON.parse(dataStr) as unknown;
103
- // S3 batched envelope — `event: batch` carries an array of events.
104
134
  if (eventType === 'batch' && Array.isArray(parsed)) {
105
- return parsed as RunEventDoc[];
135
+ return parsed as T[];
106
136
  }
107
- // Normal single-event payload.
108
- return [parsed as RunEventDoc];
137
+ return [parsed as T];
109
138
  } catch {
110
- // Skip non-JSON events (keep-alive payloads, vendor extensions).
139
+ // Skip non-JSON frames (keep-alive payloads, vendor extensions).
111
140
  return [];
112
141
  }
113
142
  };
@@ -140,15 +169,13 @@ export async function* streamEvents(
140
169
  case 'data':
141
170
  pendingData.push(fieldValue);
142
171
  break;
143
- case 'id':
144
- pendingId = fieldValue;
145
- break;
146
172
  default:
173
+ // `id:` is the sequence; the consumer reads it from the document itself.
147
174
  break;
148
175
  }
149
176
  }
150
177
  }
151
- // Flush any final unterminated event.
178
+ // Flush any final unterminated frame.
152
179
  for (const final of flushAndYield()) yield final;
153
180
  } finally {
154
181
  try {
@@ -158,10 +185,4 @@ export async function* streamEvents(
158
185
  }
159
186
  if (!internalAbort.signal.aborted) internalAbort.abort();
160
187
  }
161
-
162
- // Reference pendingEvent/pendingId so the linter doesn't flag them as
163
- // unused; they're consumed via flushAndYield's closure but TS can't see
164
- // that across a generator boundary.
165
- void pendingEvent;
166
- void pendingId;
167
188
  }