@pouchy_ai/companion-sdk 0.29.1 → 0.30.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/CHANGELOG.md +44 -1
- package/README.md +20 -3
- package/dist/client.d.ts +45 -5
- package/dist/client.js +82 -11
- package/dist/errors.d.ts +14 -1
- package/dist/errors.js +7 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -1
- package/dist/protocol.d.ts +57 -0
- package/dist/protocol.js +11 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,48 @@ a protocol bump is always called out explicitly here.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.30.0] - 2026-07-12
|
|
16
|
+
|
|
17
|
+
No breaking changes despite the minor bump — everything here is additive
|
|
18
|
+
surface (which this repo's house policy ships as `0.x.0`).
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Typed `control.error` code vocabulary.** New runtime export
|
|
23
|
+
`CONTROL_ERROR_CODES` — the server-emitted stream-plane codes
|
|
24
|
+
(`agent_error`, `call_mint_failed`) — plus the types `ControlErrorCode`,
|
|
25
|
+
`ControlErrorPayload`, and the open union `ControlErrorCodeValue` (server
|
|
26
|
+
codes + the SDK-synthesized `stream_unauthorized` + a forward-compat string
|
|
27
|
+
arm, same doctrine as `CompanionErrorCodeValue`). `onError`'s handler now
|
|
28
|
+
sees `code: ControlErrorCodeValue` instead of a bare `string`, so a `switch`
|
|
29
|
+
on it autocompletes. Drift-tested two ways: the SDK list must equal the
|
|
30
|
+
server list, AND every `control.error` emit site's code literal (server and
|
|
31
|
+
SDK) must be declared — a new site inventing a code fails CI.
|
|
32
|
+
- **`on()` narrows the envelope payload per event name.** New
|
|
33
|
+
`OutboundPayloadMap` + a generic overload:
|
|
34
|
+
`on('companion.typing', (env) => env.payload.active)` type-checks without a
|
|
35
|
+
cast; `on('*')` keeps the untyped envelope. Type-only — zero runtime change.
|
|
36
|
+
New wire-payload types exported along the way: `HelloAckPayload`,
|
|
37
|
+
`CallReadyPayload`, `ControlErrorPayload`.
|
|
38
|
+
- **Stream-state observability.** `client.streamState` +
|
|
39
|
+
`onStreamStateChange((state, prev) => …)` with
|
|
40
|
+
`'idle' | 'connecting' | 'connected' | 'reconnecting' | 'degraded_sse' |
|
|
41
|
+
'stopped'` (`CompanionStreamState`) — including the previously invisible
|
|
42
|
+
WebSocket→SSE degrade (`degraded_sse`) and the permanent-401/403 stop. Hosts
|
|
43
|
+
no longer hand-roll a connection enum around `onMessage` traffic.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **Reconnect backoff now carries jitter (additive, 0–25%)** in both the SSE
|
|
48
|
+
and WebSocket receive loops, so a fleet of embeds doesn't reconnect in
|
|
49
|
+
lockstep after a server deploy/restart (thundering herd).
|
|
50
|
+
- **`Retry-After` HTTP-date form is parsed.** RFC 9110 allows a date form
|
|
51
|
+
(proxies/CDNs emit it); it used to read as `undefined`. Delta-seconds still
|
|
52
|
+
win; a date converts to a non-negative seconds delta.
|
|
53
|
+
- **The streaming `sendText` leg keeps the 429 body `hint`.** The non-SSE
|
|
54
|
+
error fallback dropped the actionable hint (e.g. "provision a project …")
|
|
55
|
+
that the buffered path has surfaced since 0.27.0.
|
|
56
|
+
|
|
15
57
|
## [0.29.1] - 2026-07-12
|
|
16
58
|
|
|
17
59
|
### Fixed
|
|
@@ -647,7 +689,8 @@ self-hosted/CDN build; this entry captures the surface that ships publicly.
|
|
|
647
689
|
- **Versioned wire protocol.** `PROTOCOL_VERSION = 1`, kept in lockstep with the
|
|
648
690
|
server by `protocol.drift.test.ts` (fails CI on divergence).
|
|
649
691
|
|
|
650
|
-
[Unreleased]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.
|
|
692
|
+
[Unreleased]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.30.0...HEAD
|
|
693
|
+
[0.30.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.29.1...companion-sdk-v0.30.0
|
|
651
694
|
[0.29.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.29.0...companion-sdk-v0.29.1
|
|
652
695
|
[0.29.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.28.1...companion-sdk-v0.29.0
|
|
653
696
|
[0.28.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.28.0...companion-sdk-v0.28.1
|
package/README.md
CHANGED
|
@@ -63,9 +63,25 @@ Token as `token` instead — same API. See the docs' authentication section.)
|
|
|
63
63
|
- **Capabilities map:** [`docs/companion-capabilities.md`](../../docs/companion-capabilities.md)
|
|
64
64
|
- **Instant UI renderer contract:** [`docs/companion-instant-ui.md`](../../docs/companion-instant-ui.md)
|
|
65
65
|
|
|
66
|
+
### Constructor options (beyond `baseUrl` + `token`)
|
|
67
|
+
|
|
68
|
+
`createCompanion({ … })` also takes: `surface` (one resumable session per
|
|
69
|
+
surface), `modalities` / `handles` / `contextKinds` / `tools` /
|
|
70
|
+
`appContext` (the capability handshake), `visitor` (representative mode,
|
|
71
|
+
below), `onAuthError` (401 → return a fresh token and the client retries
|
|
72
|
+
transparently), `stream: 'sse' | 'websocket'` (receive transport —
|
|
73
|
+
`'websocket'` opts into the lower-latency WS plane when the deployment serves
|
|
74
|
+
one and **falls back to SSE automatically**, observable as
|
|
75
|
+
`streamState === 'degraded_sse'`), and two injection points for Node/tests:
|
|
76
|
+
`fetch` (custom fetch implementation) and `webSocketImpl` (WebSocket
|
|
77
|
+
constructor when `globalThis.WebSocket` is absent).
|
|
78
|
+
|
|
66
79
|
## Events
|
|
67
80
|
|
|
68
|
-
Subscribe with `on(type, fn)` (or `'*'`), or these typed convenience helpers
|
|
81
|
+
Subscribe with `on(type, fn)` (or `'*'`), or these typed convenience helpers.
|
|
82
|
+
Since 0.30.0 `on()` narrows the envelope payload per event name
|
|
83
|
+
(`OutboundPayloadMap`) — e.g. `on('companion.typing', (env) => env.payload.active)`
|
|
84
|
+
type-checks without a cast; `'*'` keeps the untyped envelope:
|
|
69
85
|
|
|
70
86
|
| Helper | Event | Use |
|
|
71
87
|
| --- | --- | --- |
|
|
@@ -82,7 +98,7 @@ Subscribe with `on(type, fn)` (or `'*'`), or these typed convenience helpers:
|
|
|
82
98
|
| `onTyping(fn)` | `companion.typing` | activity indicator — `fn({active})` fires `true` when a turn starts working and `false` when it finishes / pauses, spanning the tool-loop / thinking phase before the first text delta. Drive a "typing…" state |
|
|
83
99
|
| `on('control.call_ready', fn)` | `control.call_ready` | a voice call is ready — the stream echo of `startCall`'s accept. Deliberately **secret-free** (`{ provider, agentId/model, voice, … }` — the actual WebRTC credentials only ride the `startCall` HTTP response); useful for UI state on surfaces that didn't initiate the call |
|
|
84
100
|
| `onUsage(fn)` | `control.usage` | per-token metering echo. _(reserved — not emitted yet)_ |
|
|
85
|
-
| `onError(fn)` | `control.error` | agent / stream errors — `agent_error` (server-side turn failed after accept; safe to re-send), `call_mint_failed` (voice-credential mint failed — retry later or fall back to text) or the SDK-synthesized `stream_unauthorized` (stream 401 exhausted reconnects — refresh the token, `start()` again) |
|
|
101
|
+
| `onError(fn)` | `control.error` | agent / stream errors — `agent_error` (server-side turn failed after accept; safe to re-send), `call_mint_failed` (voice-credential mint failed — retry later or fall back to text) or the SDK-synthesized `stream_unauthorized` (stream 401 exhausted reconnects — refresh the token, `start()` again). Since 0.30.0 the vocabulary is EXPORTED TYPED: `CONTROL_ERROR_CODES` (runtime list, drift-tested against the server) + the `ControlErrorCodeValue` union `fn`'s `err.code` now carries — `switch` on it with autocomplete |
|
|
86
102
|
|
|
87
103
|
### Instant UI
|
|
88
104
|
|
|
@@ -112,7 +128,8 @@ local/device skills).
|
|
|
112
128
|
| `endSession()` | Distill the session into durable memory now; returns `{ skipped?: 'no_session' \| 'no_content' \| 'throttled' }`. |
|
|
113
129
|
| `close()` | One-call teardown: `stop()` + `endSession()` — the text-session mirror of the call handle's `close()`. |
|
|
114
130
|
| `startCall(opts)` / `connectCall(opts)` | Voice plane: raw credentials / fully-wired call handle (`call.close()` ends + folds the transcript into memory). |
|
|
115
|
-
| `
|
|
131
|
+
| `streamState` / `onStreamStateChange(fn)` | Receive-stream lifecycle (0.30.0): `'idle' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'degraded_sse' \| 'stopped'` (`CompanionStreamState`). `fn(state, prev)` fires on change only — drive a connection indicator; `degraded_sse` = the WS transport errored and delivery continues on the SSE fallback; `stopped` = `stop()`/`close()` or a permanent `stream_unauthorized` failure. |
|
|
132
|
+
| `getAvatar()` / `brandIconUrl(size?)` | The user's avatar (VRM/portrait) and the Pouchy brand icon for your UI. Standalone (no client/token): `pouchyBrandIconUrl(baseUrl, size?)` derives the same URL before/without connecting. |
|
|
116
133
|
| `getWallet()` | Read the instance's own wallet — `{ balances: [{ currency, amount }], totalUsd, currency }`. Read-only + receive-only; needs the `wallet.read` scope. |
|
|
117
134
|
|
|
118
135
|
Tool calling beyond your own declared tools: `HOST_CONTROL_TOOLS` (universal
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { AudioClipPayload, CompanionEnvelope, ConfirmRequestPayload, ExpressionPayload, InterfaceUpdatePayload, OutboundType, PendingConfirm, RenderInterfacePayload, SocialMessagePayload, ToolCallPayload, TypingPayload, UsagePayload, VoiceInjectPayload, WorldStateEvent } from './protocol.js';
|
|
1
|
+
import type { AudioClipPayload, CompanionEnvelope, ConfirmRequestPayload, ExpressionPayload, InterfaceUpdatePayload, OutboundPayloadMap, OutboundType, PendingConfirm, RenderInterfacePayload, SocialMessagePayload, ToolCallPayload, TypingPayload, UsagePayload, VoiceInjectPayload, WorldStateEvent } from './protocol.js';
|
|
2
2
|
/** Ergonomic world-state input: a WorldStateEvent with the CloudEvents plumbing
|
|
3
3
|
* (specversion / id / source) optional — sendWorldState fills them. */
|
|
4
4
|
export type WorldStateInput<D = unknown> = Omit<WorldStateEvent<D>, 'specversion' | 'id' | 'source'> & Partial<Pick<WorldStateEvent<D>, 'specversion' | 'id' | 'source'>>;
|
|
5
|
+
import { type ControlErrorCodeValue } from './errors.js';
|
|
5
6
|
import { type CompanionCall, type CompanionCallOptions } from './call.js';
|
|
6
7
|
export interface CompanionClientOptions {
|
|
7
8
|
/** Origin of the Pouchy deployment, e.g. "https://pouchy.ai". */
|
|
@@ -198,6 +199,18 @@ type Handler = (envelope: CompanionEnvelope) => void;
|
|
|
198
199
|
type DeltaHandler = (chunk: string, meta: {
|
|
199
200
|
reset?: boolean;
|
|
200
201
|
}) => void;
|
|
202
|
+
/** Lifecycle of the receive event stream (0.30.0) — observable via
|
|
203
|
+
* `client.streamState` + `onStreamStateChange`, so a host can render a
|
|
204
|
+
* connection indicator without hand-rolling its own enum.
|
|
205
|
+
* - `idle` — before start()
|
|
206
|
+
* - `connecting` — start() called; first connection being established
|
|
207
|
+
* - `connected` — the event stream is live (WS or SSE as configured)
|
|
208
|
+
* - `reconnecting`— connection lost / closed; backing off before the next try
|
|
209
|
+
* - `degraded_sse`— the WebSocket transport errored and delivery continues on
|
|
210
|
+
* the SSE fallback (still live — replies keep arriving)
|
|
211
|
+
* - `stopped` — stop()/close() was called, or a permanent stream failure
|
|
212
|
+
* (the `stream_unauthorized` control.error) ended the loop */
|
|
213
|
+
export type CompanionStreamState = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'degraded_sse' | 'stopped';
|
|
201
214
|
export declare class CompanionClient {
|
|
202
215
|
private readonly opts;
|
|
203
216
|
private readonly doFetch;
|
|
@@ -210,9 +223,22 @@ export declare class CompanionClient {
|
|
|
210
223
|
private readonly seen;
|
|
211
224
|
private readonly deltaHandlers;
|
|
212
225
|
private readonly pendingVoiceTools;
|
|
226
|
+
private _streamState;
|
|
227
|
+
private readonly streamStateHandlers;
|
|
213
228
|
constructor(opts: CompanionClientOptions);
|
|
214
229
|
/** The active session id, or null before connect(). */
|
|
215
230
|
get sessionId(): string | null;
|
|
231
|
+
/** Current lifecycle of the receive event stream (0.30.0) — `idle` before
|
|
232
|
+
* start(), then `connecting` / `connected` / `reconnecting` /
|
|
233
|
+
* `degraded_sse` (WS errored, SSE fallback carrying delivery) / `stopped`.
|
|
234
|
+
* Subscribe to transitions with `onStreamStateChange`. */
|
|
235
|
+
get streamState(): CompanionStreamState;
|
|
236
|
+
/** Subscribe to receive-stream lifecycle transitions (0.30.0) — drive a
|
|
237
|
+
* connection indicator: `client.onStreamStateChange((s) => setStatus(s))`.
|
|
238
|
+
* Fires only on change, with the previous state as the second argument.
|
|
239
|
+
* Returns an unsubscribe function. */
|
|
240
|
+
onStreamStateChange(handler: (state: CompanionStreamState, prev: CompanionStreamState) => void): () => void;
|
|
241
|
+
private setStreamState;
|
|
216
242
|
/** Swap the bearer token used by every subsequent request and stream
|
|
217
243
|
* reconnect. Session tokens expire (default 1h) — call this when your
|
|
218
244
|
* backend re-mints one, or wire `onAuthError` to do it on demand. */
|
|
@@ -407,9 +433,12 @@ export declare class CompanionClient {
|
|
|
407
433
|
onToolCall(handler: (call: ToolCallEvent, envelope: CompanionEnvelope) => void): () => void;
|
|
408
434
|
/** Convenience: subscribe to errors the companion surfaces — server-side agent
|
|
409
435
|
* errors and stream failures (e.g. a permanent `stream_unauthorized`) both
|
|
410
|
-
* arrive as `control.error`.
|
|
436
|
+
* arrive as `control.error`. Since 0.30.0 `code` is typed as
|
|
437
|
+
* `ControlErrorCodeValue` (the CONTROL_ERROR_CODES vocabulary + the
|
|
438
|
+
* SDK-synthesized stream codes, open for newer servers) so a `switch` on it
|
|
439
|
+
* autocompletes. Returns an unsubscribe fn. */
|
|
411
440
|
onError(handler: (err: {
|
|
412
|
-
code:
|
|
441
|
+
code: ControlErrorCodeValue;
|
|
413
442
|
message: string;
|
|
414
443
|
}, envelope: CompanionEnvelope) => void): () => void;
|
|
415
444
|
/** Recall the memory this token is authorized to see. Without `query` the
|
|
@@ -533,8 +562,12 @@ export declare class CompanionClient {
|
|
|
533
562
|
* the companion's app tile, etc. Sizes: 256 | 512 | 1024 (default 512). */
|
|
534
563
|
brandIconUrl(size?: BrandIconSize): string;
|
|
535
564
|
/** Subscribe to outbound events of a given type, or "*" for all. Returns an
|
|
536
|
-
* unsubscribe function.
|
|
537
|
-
|
|
565
|
+
* unsubscribe function. Since 0.30.0 the envelope payload is NARROWED per
|
|
566
|
+
* event name (OutboundPayloadMap) — e.g. `on('companion.typing', (env) =>
|
|
567
|
+
* env.payload.active)` type-checks without a cast; `'*'` keeps the untyped
|
|
568
|
+
* envelope. Type-only: runtime behavior is unchanged. */
|
|
569
|
+
on<T extends OutboundType>(type: T, handler: (envelope: CompanionEnvelope<OutboundPayloadMap[T]>) => void): () => void;
|
|
570
|
+
on(type: '*', handler: Handler): () => void;
|
|
538
571
|
/** Convenience: subscribe to assistant text replies. */
|
|
539
572
|
onMessage(handler: (text: string, envelope: CompanionEnvelope) => void): () => void;
|
|
540
573
|
/** Convenience: subscribe to Instant UI render surfaces (companion.ui_action).
|
|
@@ -627,7 +660,14 @@ export declare class CompanionClient {
|
|
|
627
660
|
stop(): void;
|
|
628
661
|
private emit;
|
|
629
662
|
private handleFrame;
|
|
663
|
+
/** SSE receive loop. `degraded` marks that this loop is the WS transport's
|
|
664
|
+
* fallback, so its healthy state reads `degraded_sse` (still live) instead
|
|
665
|
+
* of `connected` — the host can tell "fine" from "fine, but on plan B". */
|
|
630
666
|
private loop;
|
|
631
667
|
private consume;
|
|
632
668
|
private sleep;
|
|
669
|
+
/** Backoff sleep with additive jitter (0–25%, 0.30.0) so a fleet of embeds
|
|
670
|
+
* doesn't reconnect in lockstep after a server deploy/restart (thundering
|
|
671
|
+
* herd) — every reconnect path sleeps through here, never bare sleep(). */
|
|
672
|
+
private backoffSleep;
|
|
633
673
|
}
|
package/dist/client.js
CHANGED
|
@@ -62,6 +62,9 @@ export class CompanionClient {
|
|
|
62
62
|
deltaHandlers = new Set();
|
|
63
63
|
// Voice tool-calls awaiting the app's sendToolResult, keyed by a synthetic id.
|
|
64
64
|
pendingVoiceTools = new Map();
|
|
65
|
+
// Receive-stream lifecycle (0.30.0) — see CompanionStreamState.
|
|
66
|
+
_streamState = 'idle';
|
|
67
|
+
streamStateHandlers = new Set();
|
|
65
68
|
constructor(opts) {
|
|
66
69
|
if (!opts.baseUrl)
|
|
67
70
|
throw new CompanionError('CompanionClient: baseUrl is required', 0, 'missing_option');
|
|
@@ -74,6 +77,37 @@ export class CompanionClient {
|
|
|
74
77
|
get sessionId() {
|
|
75
78
|
return this._session;
|
|
76
79
|
}
|
|
80
|
+
/** Current lifecycle of the receive event stream (0.30.0) — `idle` before
|
|
81
|
+
* start(), then `connecting` / `connected` / `reconnecting` /
|
|
82
|
+
* `degraded_sse` (WS errored, SSE fallback carrying delivery) / `stopped`.
|
|
83
|
+
* Subscribe to transitions with `onStreamStateChange`. */
|
|
84
|
+
get streamState() {
|
|
85
|
+
return this._streamState;
|
|
86
|
+
}
|
|
87
|
+
/** Subscribe to receive-stream lifecycle transitions (0.30.0) — drive a
|
|
88
|
+
* connection indicator: `client.onStreamStateChange((s) => setStatus(s))`.
|
|
89
|
+
* Fires only on change, with the previous state as the second argument.
|
|
90
|
+
* Returns an unsubscribe function. */
|
|
91
|
+
onStreamStateChange(handler) {
|
|
92
|
+
this.streamStateHandlers.add(handler);
|
|
93
|
+
return () => this.streamStateHandlers.delete(handler);
|
|
94
|
+
}
|
|
95
|
+
setStreamState(next) {
|
|
96
|
+
if (next === this._streamState)
|
|
97
|
+
return;
|
|
98
|
+
const prev = this._streamState;
|
|
99
|
+
this._streamState = next;
|
|
100
|
+
// Same isolation doctrine as emit(): a throwing consumer handler must not
|
|
101
|
+
// skip the remaining handlers or bubble into the stream loop.
|
|
102
|
+
for (const h of this.streamStateHandlers) {
|
|
103
|
+
try {
|
|
104
|
+
h(next, prev);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
console.error('[companion-sdk] stream-state handler threw', e);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
77
111
|
/** Swap the bearer token used by every subsequent request and stream
|
|
78
112
|
* reconnect. Session tokens expire (default 1h) — call this when your
|
|
79
113
|
* backend re-mints one, or wire `onAuthError` to do it on demand. */
|
|
@@ -198,7 +232,14 @@ export class CompanionClient {
|
|
|
198
232
|
if (raw === null || raw.trim() === '')
|
|
199
233
|
return undefined;
|
|
200
234
|
const header = Number(raw);
|
|
201
|
-
|
|
235
|
+
if (Number.isFinite(header) && header >= 0)
|
|
236
|
+
return header;
|
|
237
|
+
// RFC 9110 also allows an HTTP-date (proxies/CDNs emit it) — convert to a
|
|
238
|
+
// non-negative seconds delta (0.30.0). Unparseable stays undefined.
|
|
239
|
+
const at = Date.parse(raw);
|
|
240
|
+
if (!Number.isNaN(at))
|
|
241
|
+
return Math.max(0, Math.ceil((at - Date.now()) / 1000));
|
|
242
|
+
return undefined;
|
|
202
243
|
}
|
|
203
244
|
async postJson(path, body, signal) {
|
|
204
245
|
const res = await this.fetchAuthed(this.url(path), {
|
|
@@ -388,7 +429,11 @@ export class CompanionClient {
|
|
|
388
429
|
// Old server / error before the stream opened — behave like postJson.
|
|
389
430
|
const json = (await res.json().catch(() => null));
|
|
390
431
|
if (!res.ok || !json || json.ok === false) {
|
|
391
|
-
|
|
432
|
+
// Same as postJson: the 429 body's actionable `hint` (e.g. "provision
|
|
433
|
+
// a project …") must survive the streaming leg too (0.30.0).
|
|
434
|
+
const hint = json?.hint;
|
|
435
|
+
const detail = typeof hint === 'string' && hint ? ` — ${hint}` : '';
|
|
436
|
+
throw new CompanionError((json?.error || `request failed (${res.status})`) + detail, res.status, json?.code, this.retryAfterFrom(res, json));
|
|
392
437
|
}
|
|
393
438
|
return { seq: json.seq ?? null };
|
|
394
439
|
}
|
|
@@ -770,7 +815,10 @@ export class CompanionClient {
|
|
|
770
815
|
}
|
|
771
816
|
/** Convenience: subscribe to errors the companion surfaces — server-side agent
|
|
772
817
|
* errors and stream failures (e.g. a permanent `stream_unauthorized`) both
|
|
773
|
-
* arrive as `control.error`.
|
|
818
|
+
* arrive as `control.error`. Since 0.30.0 `code` is typed as
|
|
819
|
+
* `ControlErrorCodeValue` (the CONTROL_ERROR_CODES vocabulary + the
|
|
820
|
+
* SDK-synthesized stream codes, open for newer servers) so a `switch` on it
|
|
821
|
+
* autocompletes. Returns an unsubscribe fn. */
|
|
774
822
|
onError(handler) {
|
|
775
823
|
return this.on('control.error', (env) => {
|
|
776
824
|
const p = env.payload;
|
|
@@ -940,8 +988,6 @@ export class CompanionClient {
|
|
|
940
988
|
brandIconUrl(size = 512) {
|
|
941
989
|
return pouchyBrandIconUrl(this.opts.baseUrl, size);
|
|
942
990
|
}
|
|
943
|
-
/** Subscribe to outbound events of a given type, or "*" for all. Returns an
|
|
944
|
-
* unsubscribe function. */
|
|
945
991
|
on(type, handler) {
|
|
946
992
|
let set = this.handlers.get(type);
|
|
947
993
|
if (!set) {
|
|
@@ -1096,6 +1142,7 @@ export class CompanionClient {
|
|
|
1096
1142
|
return;
|
|
1097
1143
|
this.requireSession();
|
|
1098
1144
|
this.streaming = true;
|
|
1145
|
+
this.setStreamState('connecting');
|
|
1099
1146
|
const gen = ++this.streamGen;
|
|
1100
1147
|
const wsImpl = this.opts.webSocketImpl ?? globalThis.WebSocket;
|
|
1101
1148
|
if (this.opts.stream === 'websocket' && wsImpl) {
|
|
@@ -1126,6 +1173,10 @@ export class CompanionClient {
|
|
|
1126
1173
|
const ws = new WS(deriveWsStreamUrl(this.opts.baseUrl, id, this.opts.token, this.cursor));
|
|
1127
1174
|
let poll;
|
|
1128
1175
|
let settled = false;
|
|
1176
|
+
ws.onopen = () => {
|
|
1177
|
+
if (this.streaming && gen === this.streamGen)
|
|
1178
|
+
this.setStreamState('connected');
|
|
1179
|
+
};
|
|
1129
1180
|
const settle = (err) => {
|
|
1130
1181
|
if (poll) {
|
|
1131
1182
|
clearInterval(poll);
|
|
@@ -1169,8 +1220,12 @@ export class CompanionClient {
|
|
|
1169
1220
|
}
|
|
1170
1221
|
if (errored) {
|
|
1171
1222
|
// Socket unavailable / rejected — degrade to the robust SSE loop (once).
|
|
1172
|
-
|
|
1173
|
-
|
|
1223
|
+
// The SSE loop's healthy state then reads `degraded_sse`, so the host
|
|
1224
|
+
// can observe the (previously invisible) WS→SSE degrade.
|
|
1225
|
+
if (this.streaming && gen === this.streamGen) {
|
|
1226
|
+
this.setStreamState('reconnecting');
|
|
1227
|
+
await this.loop(gen, true);
|
|
1228
|
+
}
|
|
1174
1229
|
return;
|
|
1175
1230
|
}
|
|
1176
1231
|
// Clean close: the while-guard reconnects if still streaming, else exits.
|
|
@@ -1178,8 +1233,10 @@ export class CompanionClient {
|
|
|
1178
1233
|
// closes — auth-as-close, LB idle policy, misconfigured proxy) must
|
|
1179
1234
|
// back off like the SSE loop, or this reconnects in a zero-delay hot
|
|
1180
1235
|
// loop. A connection that lived a while resets the backoff.
|
|
1236
|
+
if (this.streaming && gen === this.streamGen)
|
|
1237
|
+
this.setStreamState('reconnecting');
|
|
1181
1238
|
if (Date.now() - connectedAt < 2000) {
|
|
1182
|
-
await this.
|
|
1239
|
+
await this.backoffSleep(backoff);
|
|
1183
1240
|
backoff = Math.min(backoff * 2, 8000);
|
|
1184
1241
|
}
|
|
1185
1242
|
else {
|
|
@@ -1193,6 +1250,7 @@ export class CompanionClient {
|
|
|
1193
1250
|
this.streamGen++; // invalidate any loop still unwinding
|
|
1194
1251
|
this.abort?.abort();
|
|
1195
1252
|
this.abort = null;
|
|
1253
|
+
this.setStreamState('stopped');
|
|
1196
1254
|
}
|
|
1197
1255
|
emit(envelope) {
|
|
1198
1256
|
// Dedup by envelope id — an ungraceful drop reconnects from the last KNOWN
|
|
@@ -1277,7 +1335,10 @@ export class CompanionClient {
|
|
|
1277
1335
|
if (env && typeof env.type === 'string')
|
|
1278
1336
|
this.emit(env);
|
|
1279
1337
|
}
|
|
1280
|
-
|
|
1338
|
+
/** SSE receive loop. `degraded` marks that this loop is the WS transport's
|
|
1339
|
+
* fallback, so its healthy state reads `degraded_sse` (still live) instead
|
|
1340
|
+
* of `connected` — the host can tell "fine" from "fine, but on plan B". */
|
|
1341
|
+
async loop(gen, degraded = false) {
|
|
1281
1342
|
const id = this.requireSession();
|
|
1282
1343
|
let backoff = 500;
|
|
1283
1344
|
let authRefreshes = 0; // consecutive 401→refresh cycles without a healthy connect
|
|
@@ -1311,23 +1372,27 @@ export class CompanionClient {
|
|
|
1311
1372
|
message: `event stream rejected (${res.status}); the token likely lacks the "events.subscribe" scope`
|
|
1312
1373
|
}
|
|
1313
1374
|
});
|
|
1375
|
+
this.setStreamState('stopped');
|
|
1314
1376
|
break;
|
|
1315
1377
|
}
|
|
1316
1378
|
if (!res.ok || !res.body) {
|
|
1317
1379
|
if (!this.streaming || gen !== this.streamGen)
|
|
1318
1380
|
break;
|
|
1319
|
-
|
|
1381
|
+
this.setStreamState('reconnecting');
|
|
1382
|
+
await this.backoffSleep(backoff);
|
|
1320
1383
|
backoff = Math.min(backoff * 2, 8000);
|
|
1321
1384
|
continue;
|
|
1322
1385
|
}
|
|
1323
1386
|
backoff = 500; // healthy connection resets backoff
|
|
1324
1387
|
authRefreshes = 0;
|
|
1388
|
+
this.setStreamState(degraded ? 'degraded_sse' : 'connected');
|
|
1325
1389
|
await this.consume(res.body, gen);
|
|
1326
1390
|
}
|
|
1327
1391
|
catch {
|
|
1328
1392
|
if (!this.streaming || gen !== this.streamGen)
|
|
1329
1393
|
break;
|
|
1330
|
-
|
|
1394
|
+
this.setStreamState('reconnecting');
|
|
1395
|
+
await this.backoffSleep(backoff);
|
|
1331
1396
|
backoff = Math.min(backoff * 2, 8000);
|
|
1332
1397
|
}
|
|
1333
1398
|
}
|
|
@@ -1350,4 +1415,10 @@ export class CompanionClient {
|
|
|
1350
1415
|
sleep(ms) {
|
|
1351
1416
|
return new Promise((res) => setTimeout(res, ms));
|
|
1352
1417
|
}
|
|
1418
|
+
/** Backoff sleep with additive jitter (0–25%, 0.30.0) so a fleet of embeds
|
|
1419
|
+
* doesn't reconnect in lockstep after a server deploy/restart (thundering
|
|
1420
|
+
* herd) — every reconnect path sleeps through here, never bare sleep(). */
|
|
1421
|
+
backoffSleep(ms) {
|
|
1422
|
+
return this.sleep(ms + Math.floor(Math.random() * ms * 0.25));
|
|
1423
|
+
}
|
|
1353
1424
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import type { CompanionErrorCode } from './protocol.js';
|
|
1
|
+
import type { CompanionErrorCode, ControlErrorCode } from './protocol.js';
|
|
2
2
|
/** Codes the SDK itself synthesizes (status 0 — never sent by the server).
|
|
3
3
|
* Runtime array so the drift test can assert every `new CompanionError(...)`
|
|
4
4
|
* call site uses a declared code; APPEND-ONLY like the server vocabulary. */
|
|
5
5
|
export declare const SDK_SYNTHESIZED_ERROR_CODES: readonly ["reply_timeout", "needs_event_stream", "not_connected", "missing_option", "not_representative", "call_unsupported", "call_connect_failed", "call_dependency_missing", "aborted"];
|
|
6
6
|
export type SdkSynthesizedErrorCode = (typeof SDK_SYNTHESIZED_ERROR_CODES)[number];
|
|
7
|
+
/** `control.error` STREAM codes the SDK itself synthesizes (never sent by the
|
|
8
|
+
* server) — the stream-plane sibling of SDK_SYNTHESIZED_ERROR_CODES above.
|
|
9
|
+
* Runtime array so the drift test can assert every locally-emitted
|
|
10
|
+
* `control.error` envelope uses a declared code; APPEND-ONLY. */
|
|
11
|
+
export declare const SDK_STREAM_ERROR_CODES: readonly ["stream_unauthorized"];
|
|
12
|
+
export type SdkStreamErrorCode = (typeof SDK_STREAM_ERROR_CODES)[number];
|
|
13
|
+
/** `control.error` `code` values a consumer can switch on (0.30.0): the server
|
|
14
|
+
* vocabulary (CONTROL_ERROR_CODES, drift-tested against the server list) plus
|
|
15
|
+
* the SDK-synthesized stream codes — DERIVED from the runtime lists rather
|
|
16
|
+
* than hand-retyped, same doctrine as CompanionErrorCodeValue below. The
|
|
17
|
+
* `(string & {})` arm keeps the type open for codes newer than this SDK build
|
|
18
|
+
* while preserving autocomplete. */
|
|
19
|
+
export type ControlErrorCodeValue = ControlErrorCode | SdkStreamErrorCode | (string & {});
|
|
7
20
|
/** Thrown by every helper on an HTTP or client-side failure. `status` is the
|
|
8
21
|
* HTTP status (0 for client-synthesized failures — timeouts, unsupported
|
|
9
22
|
* environments, missing optional deps); `code` is machine-switchable when
|
package/dist/errors.js
CHANGED
|
@@ -17,6 +17,13 @@ export const SDK_SYNTHESIZED_ERROR_CODES = [
|
|
|
17
17
|
'call_dependency_missing', // optional voice dependency not installed
|
|
18
18
|
'aborted' // a caller-supplied AbortSignal cancelled the request (0.29.0)
|
|
19
19
|
];
|
|
20
|
+
/** `control.error` STREAM codes the SDK itself synthesizes (never sent by the
|
|
21
|
+
* server) — the stream-plane sibling of SDK_SYNTHESIZED_ERROR_CODES above.
|
|
22
|
+
* Runtime array so the drift test can assert every locally-emitted
|
|
23
|
+
* `control.error` envelope uses a declared code; APPEND-ONLY. */
|
|
24
|
+
export const SDK_STREAM_ERROR_CODES = [
|
|
25
|
+
'stream_unauthorized' // the event stream got a permanent 401/403 and reconnect gave up
|
|
26
|
+
];
|
|
20
27
|
export class CompanionError extends Error {
|
|
21
28
|
status;
|
|
22
29
|
code;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { CompanionClient, type CompanionClientOptions } from './client.js';
|
|
2
2
|
export { CompanionClient, CompanionError, pouchyBrandIconUrl } from './client.js';
|
|
3
|
-
export type { CompanionErrorCodeValue } from './errors.js';
|
|
4
|
-
export type { CompanionClientOptions, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, ToolCallEvent, WorldStateInput, CompanionAvatar, CompanionWallet, WalletBalance, BrandIconSize, EndSessionResult, SendTextReply } from './client.js';
|
|
3
|
+
export type { CompanionErrorCodeValue, ControlErrorCodeValue } from './errors.js';
|
|
4
|
+
export type { CompanionClientOptions, CompanionStreamState, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, ToolCallEvent, WorldStateInput, CompanionAvatar, CompanionWallet, WalletBalance, BrandIconSize, EndSessionResult, SendTextReply } from './client.js';
|
|
5
5
|
export { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_VISUAL_TOOLS, AVATAR_VISUAL_TOOL_NAMES } from './call.js';
|
|
6
6
|
export type { CompanionCall, CompanionCallOptions, VoiceToolBridge } from './call.js';
|
|
7
7
|
export { COMPANION_SCOPES, COMPANION_MODALITIES, SENSITIVE_SCOPES, REPRESENT_SCOPES, DEFAULT_SCOPES, DEFAULT_MODALITIES, isSensitiveScope, isRepresentScope, hasScope } from './scopes.js';
|
|
8
8
|
export type { CompanionScope, CompanionModality } from './scopes.js';
|
|
9
|
-
export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, ToolCallPayload, AudioClipPayload, ExpressionPayload, TypingPayload, VoiceInjectPayload, UsagePayload, MessagePayload } from './protocol.js';
|
|
10
|
-
export { PROTOCOL_VERSION, INBOUND_TYPES, OUTBOUND_TYPES, COMPANION_ERROR_CODES } from './protocol.js';
|
|
11
|
-
export type { CompanionErrorCode } from './protocol.js';
|
|
9
|
+
export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, ToolCallPayload, AudioClipPayload, ExpressionPayload, TypingPayload, VoiceInjectPayload, UsagePayload, MessagePayload, ControlErrorPayload, HelloAckPayload, CallReadyPayload, OutboundPayloadMap } from './protocol.js';
|
|
10
|
+
export { PROTOCOL_VERSION, INBOUND_TYPES, OUTBOUND_TYPES, COMPANION_ERROR_CODES, CONTROL_ERROR_CODES } from './protocol.js';
|
|
11
|
+
export type { CompanionErrorCode, ControlErrorCode } from './protocol.js';
|
|
12
12
|
/** Create a companion client. */
|
|
13
13
|
export declare function createCompanion(opts: CompanionClientOptions): CompanionClient;
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,9 @@ export { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_
|
|
|
15
15
|
export { COMPANION_SCOPES, COMPANION_MODALITIES, SENSITIVE_SCOPES, REPRESENT_SCOPES, DEFAULT_SCOPES, DEFAULT_MODALITIES, isSensitiveScope, isRepresentScope, hasScope } from './scopes.js';
|
|
16
16
|
// Runtime protocol constants — so a host can assert PROTOCOL_VERSION or
|
|
17
17
|
// enumerate/validate the event vocabulary without hard-coding the strings.
|
|
18
|
-
|
|
18
|
+
// CONTROL_ERROR_CODES (0.30.0) is the stream-plane control.error vocabulary —
|
|
19
|
+
// a separate, smaller list than the HTTP COMPANION_ERROR_CODES.
|
|
20
|
+
export { PROTOCOL_VERSION, INBOUND_TYPES, OUTBOUND_TYPES, COMPANION_ERROR_CODES, CONTROL_ERROR_CODES } from './protocol.js';
|
|
19
21
|
/** Create a companion client. */
|
|
20
22
|
export function createCompanion(opts) {
|
|
21
23
|
return new CompanionClient(opts);
|
package/dist/protocol.d.ts
CHANGED
|
@@ -12,6 +12,15 @@ export type InboundType = (typeof INBOUND_TYPES)[number];
|
|
|
12
12
|
export declare const OUTBOUND_TYPES: readonly ["hello.ack", "companion.message", "companion.audio", "companion.tool_call", "companion.ui_action", "companion.ui_update", "companion.expression", "companion.voice_inject", "companion.confirm_request", "companion.social_message", "companion.typing", "control.call_ready", "control.error", "control.usage"];
|
|
13
13
|
export type OutboundType = (typeof OUTBOUND_TYPES)[number];
|
|
14
14
|
export type MessageType = InboundType | OutboundType;
|
|
15
|
+
/** `control.error` STREAM-plane code vocabulary (0.30.0) — a separate, smaller
|
|
16
|
+
* vocabulary from COMPANION_ERROR_CODES below (those tag HTTP `{ok:false}`
|
|
17
|
+
* response bodies; these tag `control.error` envelopes on the event stream,
|
|
18
|
+
* surfaced by `onError`). Self-contained mirror of the server list
|
|
19
|
+
* (drift-tested); APPEND-ONLY. The SDK itself synthesizes one more,
|
|
20
|
+
* `stream_unauthorized` (errors.ts) — the public union `ControlErrorCodeValue`
|
|
21
|
+
* covers both plus a forward-compat string arm. */
|
|
22
|
+
export declare const CONTROL_ERROR_CODES: readonly ["agent_error", "call_mint_failed"];
|
|
23
|
+
export type ControlErrorCode = (typeof CONTROL_ERROR_CODES)[number];
|
|
15
24
|
/** HTTP error `code` vocabulary — what `CompanionError.code` can hold. A
|
|
16
25
|
* self-contained mirror of the server's api-error.ts list (drift-tested);
|
|
17
26
|
* APPEND-ONLY: renaming or removing a code is a breaking SDK change.
|
|
@@ -187,3 +196,51 @@ export interface WorldStateEvent<D = unknown> {
|
|
|
187
196
|
salience?: number;
|
|
188
197
|
voiceRelevant?: boolean;
|
|
189
198
|
}
|
|
199
|
+
/** Payload of a `control.error` event — a typed error on the event stream
|
|
200
|
+
* (surfaced by `onError`). `code` is one of CONTROL_ERROR_CODES, the
|
|
201
|
+
* SDK-synthesized `stream_unauthorized`, or a code newer than this SDK build
|
|
202
|
+
* (the open arm keeps forward compat without losing autocomplete). */
|
|
203
|
+
export interface ControlErrorPayload {
|
|
204
|
+
code: ControlErrorCode | (string & {});
|
|
205
|
+
message: string;
|
|
206
|
+
}
|
|
207
|
+
/** Payload of a `hello.ack` — the handshake reply. On the REST plane this is
|
|
208
|
+
* the `/session` response body (what `connect()` normalizes into `HelloAck`);
|
|
209
|
+
* the event stream's greeting `open` frame carries a partial echo (just
|
|
210
|
+
* `{ resumeCursor }`), so every field is optional at the wire level. */
|
|
211
|
+
export interface HelloAckPayload {
|
|
212
|
+
session?: string;
|
|
213
|
+
grantedScopes?: string[];
|
|
214
|
+
modalities?: string[];
|
|
215
|
+
resumeCursor?: number;
|
|
216
|
+
representative?: boolean;
|
|
217
|
+
visitorPaired?: boolean;
|
|
218
|
+
}
|
|
219
|
+
/** Payload of a `control.call_ready` event — the stream echo of `start_call`'s
|
|
220
|
+
* accept. Deliberately SECRET-FREE (`provider` plus non-secret metadata such
|
|
221
|
+
* as `agentId` / `model` / `voice`); the actual WebRTC credentials only ride
|
|
222
|
+
* the `startCall` HTTP response. */
|
|
223
|
+
export interface CallReadyPayload {
|
|
224
|
+
provider: string;
|
|
225
|
+
[k: string]: unknown;
|
|
226
|
+
}
|
|
227
|
+
/** Per-event payload types, keyed by outbound event name — what `client.on()`
|
|
228
|
+
* narrows envelopes with (0.30.0). Type-only; extending the base Record keeps
|
|
229
|
+
* indexing valid for every OutboundType, so an event added to the vocabulary
|
|
230
|
+
* before it gets a dedicated payload type simply reads as `unknown`. */
|
|
231
|
+
export interface OutboundPayloadMap extends Record<OutboundType, unknown> {
|
|
232
|
+
'hello.ack': HelloAckPayload;
|
|
233
|
+
'companion.message': MessagePayload;
|
|
234
|
+
'companion.audio': AudioClipPayload;
|
|
235
|
+
'companion.tool_call': ToolCallPayload;
|
|
236
|
+
'companion.ui_action': RenderInterfacePayload;
|
|
237
|
+
'companion.ui_update': InterfaceUpdatePayload;
|
|
238
|
+
'companion.expression': ExpressionPayload;
|
|
239
|
+
'companion.voice_inject': VoiceInjectPayload;
|
|
240
|
+
'companion.confirm_request': ConfirmRequestPayload;
|
|
241
|
+
'companion.social_message': SocialMessagePayload;
|
|
242
|
+
'companion.typing': TypingPayload;
|
|
243
|
+
'control.call_ready': CallReadyPayload;
|
|
244
|
+
'control.error': ControlErrorPayload;
|
|
245
|
+
'control.usage': UsagePayload;
|
|
246
|
+
}
|
package/dist/protocol.js
CHANGED
|
@@ -40,6 +40,17 @@ export const OUTBOUND_TYPES = [
|
|
|
40
40
|
'control.error',
|
|
41
41
|
'control.usage' // reserved — not emitted yet
|
|
42
42
|
];
|
|
43
|
+
/** `control.error` STREAM-plane code vocabulary (0.30.0) — a separate, smaller
|
|
44
|
+
* vocabulary from COMPANION_ERROR_CODES below (those tag HTTP `{ok:false}`
|
|
45
|
+
* response bodies; these tag `control.error` envelopes on the event stream,
|
|
46
|
+
* surfaced by `onError`). Self-contained mirror of the server list
|
|
47
|
+
* (drift-tested); APPEND-ONLY. The SDK itself synthesizes one more,
|
|
48
|
+
* `stream_unauthorized` (errors.ts) — the public union `ControlErrorCodeValue`
|
|
49
|
+
* covers both plus a forward-compat string arm. */
|
|
50
|
+
export const CONTROL_ERROR_CODES = [
|
|
51
|
+
'agent_error', // a server-side turn failed after the input was accepted — no reply was produced; safe to re-send
|
|
52
|
+
'call_mint_failed' // start_call couldn't mint the voice-provider credential — retry later or fall back to text
|
|
53
|
+
];
|
|
43
54
|
/** HTTP error `code` vocabulary — what `CompanionError.code` can hold. A
|
|
44
55
|
* self-contained mirror of the server's api-error.ts list (drift-tested);
|
|
45
56
|
* APPEND-ONLY: renaming or removing a code is a breaking SDK change.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pouchy_ai/companion-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Embed the Pouchy companion — chat, voice, tools, memory, live world-state, instant UI, and agent-to-agent messaging — in any app, game, or site.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|