@openwop/openwop 1.2.0 → 1.4.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/src/client.ts CHANGED
@@ -12,6 +12,13 @@ import { streamEvents, type EventsStreamOptions } from './sse.js';
12
12
  import {
13
13
  WopError,
14
14
  type AuditVerifyResult,
15
+ type CreateTriggerSubscriptionResponse,
16
+ type LocalizedContentLanguageSettings,
17
+ type LocalizedContentPage,
18
+ type LocalizedContentPageResponse,
19
+ type LocalizedContentSection,
20
+ type PutContentSectionRequest,
21
+ type TriggerSubscriptionRegistration,
15
22
  type BulkCancelRunsRequest,
16
23
  type BulkCancelRunsResponse,
17
24
  type Capabilities,
@@ -837,6 +844,108 @@ export class OpenwopClient {
837
844
  },
838
845
  };
839
846
 
847
+ // ── RFC 0103 Localized content surface (gated on capabilities.content) ──
848
+ readonly content = {
849
+ /** `GET /v1/content/pages` — list page records. Returns `null` when the
850
+ * host doesn't advertise `capabilities.content` (501). */
851
+ listPages: async (): Promise<readonly LocalizedContentPage[] | null> => {
852
+ try {
853
+ return await this.#request<readonly LocalizedContentPage[]>({
854
+ method: 'GET',
855
+ path: '/v1/content/pages',
856
+ });
857
+ } catch (err) {
858
+ if (err instanceof WopError && err.status === 501) return null;
859
+ throw err;
860
+ }
861
+ },
862
+
863
+ /** `GET /v1/content/pages/{slug}` — the negotiated locale's resolved page +
864
+ * sections. `acceptLanguage` rides the `Accept-Language` header (the Stable
865
+ * `i18n.md` negotiation; no `?locale=`). Returns `null` on `404`
866
+ * (no such published page) or `501` (uncapable). */
867
+ getPage: async (
868
+ slug: string,
869
+ acceptLanguage?: string,
870
+ ): Promise<LocalizedContentPageResponse | null> => {
871
+ try {
872
+ return await this.#request<LocalizedContentPageResponse>({
873
+ method: 'GET',
874
+ path: `/v1/content/pages/${encodeURIComponent(slug)}`,
875
+ ...(acceptLanguage
876
+ ? { headers: { 'Accept-Language': acceptLanguage } }
877
+ : {}),
878
+ });
879
+ } catch (err) {
880
+ if (err instanceof WopError && (err.status === 404 || err.status === 501))
881
+ return null;
882
+ throw err;
883
+ }
884
+ },
885
+
886
+ /** `POST /v1/content/pages` — create a page record (admin). Throws the
887
+ * typed `WopError` on `400`/`401`/`403`. */
888
+ createPage: (body: LocalizedContentPage): Promise<LocalizedContentPage> =>
889
+ this.#request<LocalizedContentPage>({
890
+ method: 'POST',
891
+ path: '/v1/content/pages',
892
+ body,
893
+ }),
894
+
895
+ /** `PUT /v1/content/pages/{pageId}/sections/{sectionId}` — upsert a
896
+ * section's field overlay for a locale (admin). */
897
+ putSection: (
898
+ pageId: string,
899
+ sectionId: string,
900
+ body: PutContentSectionRequest,
901
+ ): Promise<LocalizedContentSection> =>
902
+ this.#request<LocalizedContentSection>({
903
+ method: 'PUT',
904
+ path: `/v1/content/pages/${encodeURIComponent(pageId)}/sections/${encodeURIComponent(sectionId)}`,
905
+ body,
906
+ }),
907
+
908
+ /** `GET /v1/content/settings` — language settings. Returns `null` when the
909
+ * host doesn't advertise `capabilities.content` (501). */
910
+ getSettings: async (): Promise<LocalizedContentLanguageSettings | null> => {
911
+ try {
912
+ return await this.#request<LocalizedContentLanguageSettings>({
913
+ method: 'GET',
914
+ path: '/v1/content/settings',
915
+ });
916
+ } catch (err) {
917
+ if (err instanceof WopError && err.status === 501) return null;
918
+ throw err;
919
+ }
920
+ },
921
+
922
+ /** `PUT /v1/content/settings` — replace language settings (admin). */
923
+ putSettings: (
924
+ body: LocalizedContentLanguageSettings,
925
+ ): Promise<LocalizedContentLanguageSettings> =>
926
+ this.#request<LocalizedContentLanguageSettings>({
927
+ method: 'PUT',
928
+ path: '/v1/content/settings',
929
+ body,
930
+ }),
931
+ };
932
+
933
+ // ── RFC 0099 Trigger subscriptions (gated on capabilities.triggerBridge) ──
934
+ readonly triggerSubscriptions = {
935
+ /** `POST /v1/trigger-subscriptions` — register an external-event trigger.
936
+ * The `binding.secret*` is returned ONCE at creation (SR-1); persist it.
937
+ * Throws the typed `WopError` on `400`/`401`/`403`, or `501` when the host
938
+ * doesn't advertise the trigger-bridge ingestion surface. */
939
+ create: (
940
+ body: TriggerSubscriptionRegistration,
941
+ ): Promise<CreateTriggerSubscriptionResponse> =>
942
+ this.#request<CreateTriggerSubscriptionResponse>({
943
+ method: 'POST',
944
+ path: '/v1/trigger-subscriptions',
945
+ body,
946
+ }),
947
+ };
948
+
840
949
  // ── Agent workspace files (RFC 0059; gated on capabilities.workspace) ──
841
950
  readonly workspace = {
842
951
  /**
@@ -36,8 +36,17 @@ import type {
36
36
  AgentToolCalledPayload,
37
37
  AgentToolReturnedPayload,
38
38
  MemoryWrittenPayload,
39
+ OutputChunkPayload,
39
40
  RunEventDoc,
40
41
  TypedRunEvent,
42
+ VoiceSpeechStartPayload,
43
+ VoiceTranscriptPayload,
44
+ VoiceEndpointCandidatePayload,
45
+ VoiceTurnCommitPayload,
46
+ VoiceSynthesisChunkPayload,
47
+ VoiceBargeInPayload,
48
+ VoiceCancelledPayload,
49
+ ChannelPresencePayload,
41
50
  } from './types.js';
42
51
 
43
52
  // ─── Type guards ────────────────────────────────────────────────────────
@@ -143,6 +152,113 @@ export function isMemoryWritten(
143
152
  );
144
153
  }
145
154
 
155
+ /** `output.chunk` / `ai.message.chunk` (RFC 0094 §D). Accepts both
156
+ * discriminators — `output.chunk` is the persisted run-event type;
157
+ * `ai.message.chunk` is the stream-mode `messages` SSE event name
158
+ * carrying the same payload per `stream-modes.md §messages`. Narrows
159
+ * when the payload carries the required `nodeId` + `runId` + `chunk`
160
+ * strings AND the boolean `isLast`. */
161
+ export function isOutputChunk(
162
+ ev: RunEventDoc,
163
+ ): ev is TypedRunEvent<OutputChunkPayload> {
164
+ if (ev.type !== 'output.chunk' && ev.type !== 'ai.message.chunk') return false;
165
+ if (!hasStringField(ev.payload, 'nodeId')) return false;
166
+ if (!hasStringField(ev.payload, 'runId')) return false;
167
+ if (!hasStringField(ev.payload, 'chunk')) return false;
168
+ return typeof (ev.payload as Record<string, unknown>).isLast === 'boolean';
169
+ }
170
+
171
+ function hasNumberField(payload: unknown, field: string): boolean {
172
+ return (
173
+ payload !== null &&
174
+ typeof payload === 'object' &&
175
+ typeof (payload as Record<string, unknown>)[field] === 'number'
176
+ );
177
+ }
178
+
179
+ /** `voice.speech_start` (RFC 0106). */
180
+ export function isVoiceSpeechStart(
181
+ ev: RunEventDoc,
182
+ ): ev is TypedRunEvent<VoiceSpeechStartPayload> {
183
+ return ev.type === 'voice.speech_start' && hasNumberField(ev.payload, 'atMs');
184
+ }
185
+
186
+ /** `voice.transcript` (RFC 0106). Narrows when `type` matches AND payload
187
+ * carries the required `text` + `isFinal` + `atMs` + `contentTrust`. The
188
+ * transcript is untrusted ingress (`voice-transcript-untrusted`). */
189
+ export function isVoiceTranscript(
190
+ ev: RunEventDoc,
191
+ ): ev is TypedRunEvent<VoiceTranscriptPayload> {
192
+ return (
193
+ ev.type === 'voice.transcript' &&
194
+ hasStringField(ev.payload, 'text') &&
195
+ typeof (ev.payload as Record<string, unknown>).isFinal === 'boolean' &&
196
+ hasNumberField(ev.payload, 'atMs') &&
197
+ (ev.payload as Record<string, unknown>).contentTrust === 'untrusted'
198
+ );
199
+ }
200
+
201
+ /** `voice.endpoint_candidate` (RFC 0106). */
202
+ export function isVoiceEndpointCandidate(
203
+ ev: RunEventDoc,
204
+ ): ev is TypedRunEvent<VoiceEndpointCandidatePayload> {
205
+ return (
206
+ ev.type === 'voice.endpoint_candidate' && hasNumberField(ev.payload, 'atMs')
207
+ );
208
+ }
209
+
210
+ /** `voice.turn_commit` (RFC 0106). Narrows when payload carries the required
211
+ * `atMs` + `finalText` (the settled transcript). */
212
+ export function isVoiceTurnCommit(
213
+ ev: RunEventDoc,
214
+ ): ev is TypedRunEvent<VoiceTurnCommitPayload> {
215
+ return (
216
+ ev.type === 'voice.turn_commit' &&
217
+ hasNumberField(ev.payload, 'atMs') &&
218
+ hasStringField(ev.payload, 'finalText')
219
+ );
220
+ }
221
+
222
+ /** `voice.synthesis_chunk` (RFC 0106). Narrows when payload carries the
223
+ * required `seq` (number) + `mimeType` (string). */
224
+ export function isVoiceSynthesisChunk(
225
+ ev: RunEventDoc,
226
+ ): ev is TypedRunEvent<VoiceSynthesisChunkPayload> {
227
+ return (
228
+ ev.type === 'voice.synthesis_chunk' &&
229
+ hasNumberField(ev.payload, 'seq') &&
230
+ hasStringField(ev.payload, 'mimeType')
231
+ );
232
+ }
233
+
234
+ /** `voice.barge_in` (RFC 0106). */
235
+ export function isVoiceBargeIn(
236
+ ev: RunEventDoc,
237
+ ): ev is TypedRunEvent<VoiceBargeInPayload> {
238
+ return ev.type === 'voice.barge_in' && hasNumberField(ev.payload, 'atMs');
239
+ }
240
+
241
+ /** `voice.cancelled` (RFC 0106). */
242
+ export function isVoiceCancelled(
243
+ ev: RunEventDoc,
244
+ ): ev is TypedRunEvent<VoiceCancelledPayload> {
245
+ return ev.type === 'voice.cancelled' && hasNumberField(ev.payload, 'atMs');
246
+ }
247
+
248
+ /** `channel.presence` (RFC 0110). EPHEMERAL — observable on the LIVE event
249
+ * stream only; ABSENT on replay / `:fork` (the host never persists presence
250
+ * to the replayable log). Narrows when payload carries `conversationId` +
251
+ * a `present` array. */
252
+ export function isChannelPresence(
253
+ ev: RunEventDoc,
254
+ ): ev is TypedRunEvent<ChannelPresencePayload> {
255
+ return (
256
+ ev.type === 'channel.presence' &&
257
+ hasStringField(ev.payload, 'conversationId') &&
258
+ Array.isArray((ev.payload as Record<string, unknown>).present)
259
+ );
260
+ }
261
+
146
262
  // ─── High-level subscription helper ─────────────────────────────────────
147
263
 
148
264
  /** Returned by {@link subscribeToAgentReasoning}. Call to cancel the
package/src/index.ts CHANGED
@@ -59,6 +59,18 @@ export type {
59
59
  AgentHandoffPayload,
60
60
  AgentDecidedPayload,
61
61
  MemoryWrittenPayload,
62
+ // RFC 0094 §D — streaming output chunk (`output.chunk` / `ai.message.chunk`)
63
+ OutputChunkPayload,
64
+ // RFC 0106 — voice.* run-event payloads
65
+ VoiceSpeechStartPayload,
66
+ VoiceTranscriptPayload,
67
+ VoiceEndpointCandidatePayload,
68
+ VoiceTurnCommitPayload,
69
+ VoiceSynthesisChunkPayload,
70
+ VoiceBargeInPayload,
71
+ VoiceCancelledPayload,
72
+ // RFC 0110 — channel.presence run-event payload
73
+ ChannelPresencePayload,
62
74
  // RFC 0027 + RFC 0028 — Prompt library (spec/v1/prompts.md)
63
75
  GetPromptRequest,
64
76
  ListPromptsRequest,
@@ -113,6 +125,15 @@ export {
113
125
  isAgentHandoff,
114
126
  isAgentDecided,
115
127
  isMemoryWritten,
128
+ isOutputChunk,
129
+ isVoiceSpeechStart,
130
+ isVoiceTranscript,
131
+ isVoiceEndpointCandidate,
132
+ isVoiceTurnCommit,
133
+ isVoiceSynthesisChunk,
134
+ isVoiceBargeIn,
135
+ isVoiceCancelled,
136
+ isChannelPresence,
116
137
  subscribeToAgentReasoning,
117
138
  } from './event-helpers.js';
118
139
  export type {
@@ -180,6 +201,7 @@ export type {
180
201
  // AI Envelope types (DRAFT v1.x — spec/v1/ai-envelope.md). Inbound LLM-emission
181
202
  // envelope, distinct from RunEventDoc (outbound) and ErrorEnvelope (host HTTP).
182
203
  export type {
204
+ A2UISurfacePayload,
183
205
  AIEnvelope,
184
206
  AIEnvelopeErrorPayload,
185
207
  ClarificationRequestPayload,
@@ -193,6 +215,17 @@ export type {
193
215
  SchemaRequestPayload,
194
216
  SchemaResponsePayload,
195
217
  ValidationDetail,
218
+ // RFC 0103 — localized content surface
219
+ LocalizedContentStatus,
220
+ LocalizedContentPage,
221
+ LocalizedContentSection,
222
+ LocalizedContentPageResponse,
223
+ LocalizedContentLanguageSettings,
224
+ PutContentSectionRequest,
225
+ // RFC 0099 — trigger subscription registration
226
+ TriggerSubscriptionRegistration,
227
+ TriggerSubscription,
228
+ CreateTriggerSubscriptionResponse,
196
229
  } from './types.js';
197
230
 
198
231
  // RFC 0030 §A `reasoning` field prompt-directive helper. Hosts that
@@ -18,11 +18,11 @@
18
18
  * the small known-non-terminal set is treated as terminal. This keeps
19
19
  * the helper correct against:
20
20
  *
21
- * - the canonical 8-member spec union (`pending` / `running` / `paused`
22
- * / `waiting-approval` / `waiting-input` / `completed` / `failed` /
23
- * `cancelled`)
24
- * - host extensions like `'planned'`, `'executing'`, `'waiting-external'`,
25
- * `'timed-out'`, `'interrupted'` which the OpenWOP engine emits
21
+ * - the canonical 10-member spec union (`pending` / `running` / `paused`
22
+ * / `waiting-approval` / `waiting-input` / `waiting-external` /
23
+ * `completed` / `failed` / `cancelling` / `cancelled`)
24
+ * - host extensions like `'planned'`, `'executing'`, `'timed-out'`,
25
+ * `'interrupted'` which the OpenWOP engine emits
26
26
  * - any future spec additions before the SDK ships an updated minor
27
27
  *
28
28
  * @module @openwop/openwop/run-helpers
@@ -44,6 +44,10 @@ export const ACTIVE_RUN_STATUSES = [
44
44
  'paused',
45
45
  'waiting-approval',
46
46
  'waiting-input',
47
+ 'waiting-external',
48
+ // RFC 0094 §B — transitional state during the cancel cascade; the run
49
+ // WILL still transition (to terminal `cancelled`), so it is active.
50
+ 'cancelling',
47
51
  ] as const;
48
52
 
49
53
  /**