@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.
- package/README.md +64 -117
- package/dist/client.d.ts +131 -245
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +233 -440
- package/dist/client.js.map +1 -1
- package/dist/cost-attribution.d.ts +2 -2
- package/dist/cost-attribution.js +2 -2
- package/dist/envelope-directive.d.ts +1 -1
- package/dist/envelope-directive.js +1 -1
- package/dist/event-helpers.js +1 -1
- package/dist/event-helpers.js.map +1 -1
- package/dist/generated.d.ts +17 -0
- package/dist/generated.d.ts.map +1 -0
- package/dist/generated.js +311 -0
- package/dist/generated.js.map +1 -0
- package/dist/index.d.ts +16 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -53
- package/dist/index.js.map +1 -1
- package/dist/run-helpers.d.ts +20 -21
- package/dist/run-helpers.d.ts.map +1 -1
- package/dist/run-helpers.js +23 -72
- package/dist/run-helpers.js.map +1 -1
- package/dist/sse.d.ts +33 -15
- package/dist/sse.d.ts.map +1 -1
- package/dist/sse.js +28 -30
- package/dist/sse.js.map +1 -1
- package/dist/types.d.ts +253 -559
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/webhook-header-families.d.ts +42 -0
- package/dist/webhook-header-families.d.ts.map +1 -0
- package/dist/webhook-header-families.js +58 -0
- package/dist/webhook-header-families.js.map +1 -0
- package/dist/webhook-helpers.browser.d.ts +15 -28
- package/dist/webhook-helpers.browser.d.ts.map +1 -1
- package/dist/webhook-helpers.browser.js +16 -27
- package/dist/webhook-helpers.browser.js.map +1 -1
- package/dist/webhook-helpers.d.ts +48 -32
- package/dist/webhook-helpers.d.ts.map +1 -1
- package/dist/webhook-helpers.js +52 -37
- package/dist/webhook-helpers.js.map +1 -1
- package/package.json +6 -4
- package/src/client.ts +255 -454
- package/src/cost-attribution.ts +2 -2
- package/src/envelope-directive.ts +1 -1
- package/src/event-helpers.ts +1 -1
- package/src/generated.ts +322 -0
- package/src/index.ts +78 -105
- package/src/run-helpers.ts +27 -85
- package/src/sse.ts +63 -42
- package/src/types.ts +268 -603
- package/src/webhook-header-families.ts +78 -0
- package/src/webhook-helpers.browser.ts +24 -28
- package/src/webhook-helpers.ts +87 -39
- package/dist/registry-helpers.d.ts +0 -118
- package/dist/registry-helpers.d.ts.map +0 -1
- package/dist/registry-helpers.js +0 -82
- package/dist/registry-helpers.js.map +0 -1
- package/src/registry-helpers.ts +0 -173
package/src/run-helpers.ts
CHANGED
|
@@ -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
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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
|
-
|
|
114
|
-
'unsupported_stream_mode',
|
|
115
|
-
'force_engine_version_forbidden',
|
|
116
|
-
'mock_provider_forbidden',
|
|
96
|
+
export type HttpErrorCode = ErrorCode;
|
|
117
97
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
'
|
|
121
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
] as const;
|
|
103
|
+
/** 1.x name for {@link isErrorCode}. */
|
|
104
|
+
export const isHttpErrorCode = isErrorCode;
|
|
164
105
|
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
|
257
|
-
*
|
|
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
|
|
3
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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 (
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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 = ():
|
|
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
|
|
135
|
+
return parsed as T[];
|
|
106
136
|
}
|
|
107
|
-
|
|
108
|
-
return [parsed as RunEventDoc];
|
|
137
|
+
return [parsed as T];
|
|
109
138
|
} catch {
|
|
110
|
-
// Skip non-JSON
|
|
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
|
|
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
|
}
|