@pouchy_ai/companion-sdk 0.29.0 → 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 +76 -1
- package/README.md +28 -9
- package/dist/client.d.ts +48 -5
- package/dist/client.js +103 -20
- 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 +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,79 @@ 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
|
+
|
|
57
|
+
## [0.29.1] - 2026-07-12
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- **`CompanionError.retryAfter` no longer reports a spurious `0`.** A missing
|
|
62
|
+
`Retry-After` header was parsed as `Number(null) === 0`, so EVERY error
|
|
63
|
+
thrown from a POST without the header (a `400 invalid_request`, a
|
|
64
|
+
`404 session_not_found`, …) carried `retryAfter: 0` — telling backoff code
|
|
65
|
+
"retry immediately" on non-retryable errors. It is now `undefined` unless
|
|
66
|
+
the body's `retryAfterSec` or a real header names a value, as documented.
|
|
67
|
+
- **`sendText({ awaitReply: true, stream: false })` now cancels its POST.**
|
|
68
|
+
The buffered leg never passed the operation's `AbortSignal` to the network
|
|
69
|
+
call, so the promise rejected on abort/deadline but the billed turn kept
|
|
70
|
+
running server-side. It now aborts with the same linked controller the
|
|
71
|
+
streaming leg uses.
|
|
72
|
+
- **Abort-listener leak on a reused `AbortSignal` in `awaitReply`.** The
|
|
73
|
+
deadline's reject listener was never removed, so a host reusing one
|
|
74
|
+
long-lived signal across many `sendText({ awaitReply, signal })` calls
|
|
75
|
+
accumulated one orphan listener per call (memory growth + Node's
|
|
76
|
+
MaxListenersExceeded warning). Removed in the same `finally` as the rest.
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- **`signal` on `sendToolResult` and `startCall`** — the two request-performing
|
|
81
|
+
methods 0.29.0's "AbortSignal on every network method" pass missed
|
|
82
|
+
(backwards-compatible addition, per this package's patch policy).
|
|
83
|
+
- **`require()` of the package now works on Node ≥ 20.17** (require-ESM): the
|
|
84
|
+
`exports` map gained a `default` condition — previously `require()` failed
|
|
85
|
+
with `ERR_PACKAGE_PATH_NOT_EXPORTED` before the feature could apply, despite
|
|
86
|
+
the README documenting it.
|
|
87
|
+
|
|
15
88
|
## [0.29.0] - 2026-07-12
|
|
16
89
|
|
|
17
90
|
### Added
|
|
@@ -616,7 +689,9 @@ self-hosted/CDN build; this entry captures the surface that ships publicly.
|
|
|
616
689
|
- **Versioned wire protocol.** `PROTOCOL_VERSION = 1`, kept in lockstep with the
|
|
617
690
|
server by `protocol.drift.test.ts` (fails CI on divergence).
|
|
618
691
|
|
|
619
|
-
[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
|
|
694
|
+
[0.29.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.29.0...companion-sdk-v0.29.1
|
|
620
695
|
[0.29.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.28.1...companion-sdk-v0.29.0
|
|
621
696
|
[0.28.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.28.0...companion-sdk-v0.28.1
|
|
622
697
|
[0.28.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.27.0...companion-sdk-v0.28.0
|
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
|
|
@@ -122,12 +139,14 @@ are offered automatically once you declare `tools`/`handles`, and
|
|
|
122
139
|
embeds receive expression cues — declare them or they no-op silently. Full
|
|
123
140
|
contract: `docs/companion-host-control.md`.
|
|
124
141
|
|
|
125
|
-
**Cancellation (0.29.0):** every request-performing method
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
**Cancellation (0.29.0, completed in 0.29.1):** every request-performing method
|
|
143
|
+
accepts an optional `AbortSignal` — including `sendToolResult(callId, result,
|
|
144
|
+
{ signal })` and `startCall({ signal })` since 0.29.1. Methods with an options
|
|
145
|
+
object take a `signal?` field (`sendText(text, { signal })`,
|
|
146
|
+
`recall({ signal })`, …); the rest take a trailing `opts`
|
|
147
|
+
(`getAvatar({ signal })`, `ping({ signal })`, …). Aborting rejects with
|
|
148
|
+
`CompanionError` `code: 'aborted'`, and for `sendText({ awaitReply })` it also
|
|
149
|
+
tears down the in-flight event stream.
|
|
131
150
|
|
|
132
151
|
```ts
|
|
133
152
|
const ac = new AbortController();
|
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. */
|
|
@@ -346,6 +372,7 @@ export declare class CompanionClient {
|
|
|
346
372
|
startCall(opts?: {
|
|
347
373
|
voice?: string;
|
|
348
374
|
locale?: string;
|
|
375
|
+
signal?: AbortSignal;
|
|
349
376
|
}): Promise<CallCredentials>;
|
|
350
377
|
/** Open a live voice call end-to-end: mints credentials (startCall) and opens a
|
|
351
378
|
* WebRTC session directly to the provider — no first-party app code needed.
|
|
@@ -388,6 +415,8 @@ export declare class CompanionClient {
|
|
|
388
415
|
sendToolResult(callId: string, result: {
|
|
389
416
|
ok?: boolean;
|
|
390
417
|
result?: unknown;
|
|
418
|
+
}, opts?: {
|
|
419
|
+
signal?: AbortSignal;
|
|
391
420
|
}): Promise<{
|
|
392
421
|
allDone: boolean;
|
|
393
422
|
}>;
|
|
@@ -404,9 +433,12 @@ export declare class CompanionClient {
|
|
|
404
433
|
onToolCall(handler: (call: ToolCallEvent, envelope: CompanionEnvelope) => void): () => void;
|
|
405
434
|
/** Convenience: subscribe to errors the companion surfaces — server-side agent
|
|
406
435
|
* errors and stream failures (e.g. a permanent `stream_unauthorized`) both
|
|
407
|
-
* 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. */
|
|
408
440
|
onError(handler: (err: {
|
|
409
|
-
code:
|
|
441
|
+
code: ControlErrorCodeValue;
|
|
410
442
|
message: string;
|
|
411
443
|
}, envelope: CompanionEnvelope) => void): () => void;
|
|
412
444
|
/** Recall the memory this token is authorized to see. Without `query` the
|
|
@@ -530,8 +562,12 @@ export declare class CompanionClient {
|
|
|
530
562
|
* the companion's app tile, etc. Sizes: 256 | 512 | 1024 (default 512). */
|
|
531
563
|
brandIconUrl(size?: BrandIconSize): string;
|
|
532
564
|
/** Subscribe to outbound events of a given type, or "*" for all. Returns an
|
|
533
|
-
* unsubscribe function.
|
|
534
|
-
|
|
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;
|
|
535
571
|
/** Convenience: subscribe to assistant text replies. */
|
|
536
572
|
onMessage(handler: (text: string, envelope: CompanionEnvelope) => void): () => void;
|
|
537
573
|
/** Convenience: subscribe to Instant UI render surfaces (companion.ui_action).
|
|
@@ -624,7 +660,14 @@ export declare class CompanionClient {
|
|
|
624
660
|
stop(): void;
|
|
625
661
|
private emit;
|
|
626
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". */
|
|
627
666
|
private loop;
|
|
628
667
|
private consume;
|
|
629
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;
|
|
630
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. */
|
|
@@ -191,8 +225,21 @@ export class CompanionClient {
|
|
|
191
225
|
const fromBody = json?.retryAfterSec;
|
|
192
226
|
if (typeof fromBody === 'number' && Number.isFinite(fromBody) && fromBody >= 0)
|
|
193
227
|
return fromBody;
|
|
194
|
-
|
|
195
|
-
|
|
228
|
+
// Missing/blank header must stay undefined — Number(null) is 0, which
|
|
229
|
+
// used to stamp retryAfter: 0 on EVERY error without the header (a 400,
|
|
230
|
+
// a 404 …), telling backoff code "retry immediately".
|
|
231
|
+
const raw = res.headers.get('Retry-After');
|
|
232
|
+
if (raw === null || raw.trim() === '')
|
|
233
|
+
return undefined;
|
|
234
|
+
const header = Number(raw);
|
|
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;
|
|
196
243
|
}
|
|
197
244
|
async postJson(path, body, signal) {
|
|
198
245
|
const res = await this.fetchAuthed(this.url(path), {
|
|
@@ -305,6 +352,9 @@ export class CompanionClient {
|
|
|
305
352
|
opts.signal.addEventListener('abort', onCallerAbort, { once: true });
|
|
306
353
|
}
|
|
307
354
|
let timer;
|
|
355
|
+
// Held so the finally can REMOVE it — a host reusing one long-lived signal
|
|
356
|
+
// across many awaitReply calls accumulated one orphan listener per call.
|
|
357
|
+
let onCallerAbortReject;
|
|
308
358
|
const deadline = new Promise((_, reject) => {
|
|
309
359
|
timer = setTimeout(() => {
|
|
310
360
|
ac.abort();
|
|
@@ -313,7 +363,8 @@ export class CompanionClient {
|
|
|
313
363
|
// The caller's signal also rejects the operation (the stream teardown
|
|
314
364
|
// alone would otherwise leave the fallback wait hanging to the timer).
|
|
315
365
|
if (opts?.signal) {
|
|
316
|
-
|
|
366
|
+
onCallerAbortReject = () => reject(new CompanionError('request aborted', 0, 'aborted'));
|
|
367
|
+
opts.signal.addEventListener('abort', onCallerAbortReject, { once: true });
|
|
317
368
|
}
|
|
318
369
|
});
|
|
319
370
|
try {
|
|
@@ -322,7 +373,10 @@ export class CompanionClient {
|
|
|
322
373
|
let pausedOnTools = false;
|
|
323
374
|
if (opts?.stream === false) {
|
|
324
375
|
const json = await Promise.race([
|
|
325
|
-
|
|
376
|
+
// ac.signal, like the streaming leg: the deadline and the caller's
|
|
377
|
+
// signal both abort it — without this the promise rejected but the
|
|
378
|
+
// billed POST kept running beyond cancellation.
|
|
379
|
+
this.postJson(`/api/companion/session/${sessionId}/input`, body, ac.signal),
|
|
326
380
|
deadline
|
|
327
381
|
]);
|
|
328
382
|
seq = json.seq ?? null;
|
|
@@ -354,6 +408,8 @@ export class CompanionClient {
|
|
|
354
408
|
clearTimeout(timer);
|
|
355
409
|
if (onCallerAbort)
|
|
356
410
|
opts?.signal?.removeEventListener('abort', onCallerAbort);
|
|
411
|
+
if (onCallerAbortReject)
|
|
412
|
+
opts?.signal?.removeEventListener('abort', onCallerAbortReject);
|
|
357
413
|
}
|
|
358
414
|
}
|
|
359
415
|
/** The streaming leg of sendText: parse the POST response's SSE frames —
|
|
@@ -373,7 +429,11 @@ export class CompanionClient {
|
|
|
373
429
|
// Old server / error before the stream opened — behave like postJson.
|
|
374
430
|
const json = (await res.json().catch(() => null));
|
|
375
431
|
if (!res.ok || !json || json.ok === false) {
|
|
376
|
-
|
|
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));
|
|
377
437
|
}
|
|
378
438
|
return { seq: json.seq ?? null };
|
|
379
439
|
}
|
|
@@ -512,10 +572,7 @@ export class CompanionClient {
|
|
|
512
572
|
*/
|
|
513
573
|
async startCall(opts) {
|
|
514
574
|
const id = this.requireSession();
|
|
515
|
-
return this.postJson(`/api/companion/session/${id}/call`, {
|
|
516
|
-
voice: opts?.voice,
|
|
517
|
-
locale: opts?.locale
|
|
518
|
-
});
|
|
575
|
+
return this.postJson(`/api/companion/session/${id}/call`, { voice: opts?.voice, locale: opts?.locale }, opts?.signal);
|
|
519
576
|
}
|
|
520
577
|
/** Open a live voice call end-to-end: mints credentials (startCall) and opens a
|
|
521
578
|
* WebRTC session directly to the provider — no first-party app code needed.
|
|
@@ -651,7 +708,7 @@ export class CompanionClient {
|
|
|
651
708
|
/** Report the result of a companion.tool_call this surface performed. When all
|
|
652
709
|
* of the turn's calls are reported, the companion resumes and the continuation
|
|
653
710
|
* (a companion.message or more tool calls) arrives on the event stream. */
|
|
654
|
-
async sendToolResult(callId, result) {
|
|
711
|
+
async sendToolResult(callId, result, opts) {
|
|
655
712
|
// Voice tool-call: resolve it locally so the result flows back to the voice
|
|
656
713
|
// provider, not to a server text turn. (Same call shape as the text path.)
|
|
657
714
|
const pending = this.pendingVoiceTools.get(callId);
|
|
@@ -660,7 +717,7 @@ export class CompanionClient {
|
|
|
660
717
|
return { allDone: true };
|
|
661
718
|
}
|
|
662
719
|
const id = this.requireSession();
|
|
663
|
-
const json = await this.postJson(`/api/companion/session/${id}/tool-result`, { callId, ok: result.ok !== false, result: result.result });
|
|
720
|
+
const json = await this.postJson(`/api/companion/session/${id}/tool-result`, { callId, ok: result.ok !== false, result: result.result }, opts?.signal);
|
|
664
721
|
return { allDone: json.allDone ?? false };
|
|
665
722
|
}
|
|
666
723
|
/** A tool the live voice call asked the app to run. Fires the SAME
|
|
@@ -758,7 +815,10 @@ export class CompanionClient {
|
|
|
758
815
|
}
|
|
759
816
|
/** Convenience: subscribe to errors the companion surfaces — server-side agent
|
|
760
817
|
* errors and stream failures (e.g. a permanent `stream_unauthorized`) both
|
|
761
|
-
* 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. */
|
|
762
822
|
onError(handler) {
|
|
763
823
|
return this.on('control.error', (env) => {
|
|
764
824
|
const p = env.payload;
|
|
@@ -928,8 +988,6 @@ export class CompanionClient {
|
|
|
928
988
|
brandIconUrl(size = 512) {
|
|
929
989
|
return pouchyBrandIconUrl(this.opts.baseUrl, size);
|
|
930
990
|
}
|
|
931
|
-
/** Subscribe to outbound events of a given type, or "*" for all. Returns an
|
|
932
|
-
* unsubscribe function. */
|
|
933
991
|
on(type, handler) {
|
|
934
992
|
let set = this.handlers.get(type);
|
|
935
993
|
if (!set) {
|
|
@@ -1084,6 +1142,7 @@ export class CompanionClient {
|
|
|
1084
1142
|
return;
|
|
1085
1143
|
this.requireSession();
|
|
1086
1144
|
this.streaming = true;
|
|
1145
|
+
this.setStreamState('connecting');
|
|
1087
1146
|
const gen = ++this.streamGen;
|
|
1088
1147
|
const wsImpl = this.opts.webSocketImpl ?? globalThis.WebSocket;
|
|
1089
1148
|
if (this.opts.stream === 'websocket' && wsImpl) {
|
|
@@ -1114,6 +1173,10 @@ export class CompanionClient {
|
|
|
1114
1173
|
const ws = new WS(deriveWsStreamUrl(this.opts.baseUrl, id, this.opts.token, this.cursor));
|
|
1115
1174
|
let poll;
|
|
1116
1175
|
let settled = false;
|
|
1176
|
+
ws.onopen = () => {
|
|
1177
|
+
if (this.streaming && gen === this.streamGen)
|
|
1178
|
+
this.setStreamState('connected');
|
|
1179
|
+
};
|
|
1117
1180
|
const settle = (err) => {
|
|
1118
1181
|
if (poll) {
|
|
1119
1182
|
clearInterval(poll);
|
|
@@ -1157,8 +1220,12 @@ export class CompanionClient {
|
|
|
1157
1220
|
}
|
|
1158
1221
|
if (errored) {
|
|
1159
1222
|
// Socket unavailable / rejected — degrade to the robust SSE loop (once).
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
+
}
|
|
1162
1229
|
return;
|
|
1163
1230
|
}
|
|
1164
1231
|
// Clean close: the while-guard reconnects if still streaming, else exits.
|
|
@@ -1166,8 +1233,10 @@ export class CompanionClient {
|
|
|
1166
1233
|
// closes — auth-as-close, LB idle policy, misconfigured proxy) must
|
|
1167
1234
|
// back off like the SSE loop, or this reconnects in a zero-delay hot
|
|
1168
1235
|
// loop. A connection that lived a while resets the backoff.
|
|
1236
|
+
if (this.streaming && gen === this.streamGen)
|
|
1237
|
+
this.setStreamState('reconnecting');
|
|
1169
1238
|
if (Date.now() - connectedAt < 2000) {
|
|
1170
|
-
await this.
|
|
1239
|
+
await this.backoffSleep(backoff);
|
|
1171
1240
|
backoff = Math.min(backoff * 2, 8000);
|
|
1172
1241
|
}
|
|
1173
1242
|
else {
|
|
@@ -1181,6 +1250,7 @@ export class CompanionClient {
|
|
|
1181
1250
|
this.streamGen++; // invalidate any loop still unwinding
|
|
1182
1251
|
this.abort?.abort();
|
|
1183
1252
|
this.abort = null;
|
|
1253
|
+
this.setStreamState('stopped');
|
|
1184
1254
|
}
|
|
1185
1255
|
emit(envelope) {
|
|
1186
1256
|
// Dedup by envelope id — an ungraceful drop reconnects from the last KNOWN
|
|
@@ -1265,7 +1335,10 @@ export class CompanionClient {
|
|
|
1265
1335
|
if (env && typeof env.type === 'string')
|
|
1266
1336
|
this.emit(env);
|
|
1267
1337
|
}
|
|
1268
|
-
|
|
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) {
|
|
1269
1342
|
const id = this.requireSession();
|
|
1270
1343
|
let backoff = 500;
|
|
1271
1344
|
let authRefreshes = 0; // consecutive 401→refresh cycles without a healthy connect
|
|
@@ -1299,23 +1372,27 @@ export class CompanionClient {
|
|
|
1299
1372
|
message: `event stream rejected (${res.status}); the token likely lacks the "events.subscribe" scope`
|
|
1300
1373
|
}
|
|
1301
1374
|
});
|
|
1375
|
+
this.setStreamState('stopped');
|
|
1302
1376
|
break;
|
|
1303
1377
|
}
|
|
1304
1378
|
if (!res.ok || !res.body) {
|
|
1305
1379
|
if (!this.streaming || gen !== this.streamGen)
|
|
1306
1380
|
break;
|
|
1307
|
-
|
|
1381
|
+
this.setStreamState('reconnecting');
|
|
1382
|
+
await this.backoffSleep(backoff);
|
|
1308
1383
|
backoff = Math.min(backoff * 2, 8000);
|
|
1309
1384
|
continue;
|
|
1310
1385
|
}
|
|
1311
1386
|
backoff = 500; // healthy connection resets backoff
|
|
1312
1387
|
authRefreshes = 0;
|
|
1388
|
+
this.setStreamState(degraded ? 'degraded_sse' : 'connected');
|
|
1313
1389
|
await this.consume(res.body, gen);
|
|
1314
1390
|
}
|
|
1315
1391
|
catch {
|
|
1316
1392
|
if (!this.streaming || gen !== this.streamGen)
|
|
1317
1393
|
break;
|
|
1318
|
-
|
|
1394
|
+
this.setStreamState('reconnecting');
|
|
1395
|
+
await this.backoffSleep(backoff);
|
|
1319
1396
|
backoff = Math.min(backoff * 2, 8000);
|
|
1320
1397
|
}
|
|
1321
1398
|
}
|
|
@@ -1338,4 +1415,10 @@ export class CompanionClient {
|
|
|
1338
1415
|
sleep(ms) {
|
|
1339
1416
|
return new Promise((res) => setTimeout(res, ms));
|
|
1340
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
|
+
}
|
|
1341
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pouchy_ai/companion-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Embed the Pouchy companion
|
|
3
|
+
"version": "0.30.0",
|
|
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",
|
|
7
7
|
"homepage": "https://pouchy.ai",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
|
-
"import": "./dist/index.js"
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"default": "./dist/index.js"
|
|
20
21
|
}
|
|
21
22
|
},
|
|
22
23
|
"main": "./dist/index.js",
|