@pouchy_ai/companion-sdk 0.25.0 → 0.26.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 CHANGED
@@ -12,6 +12,26 @@ a protocol bump is always called out explicitly here.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.26.0] - 2026-07-11
16
+
17
+ ### Fixed
18
+
19
+ - **Voice-call connect failures now honor the typed-error contract.** The
20
+ call helpers (`connectCall` / `startCall` / `openCompanionCall`) used to
21
+ reject with plain `Error`s for the connect-step failures, so they carried
22
+ no `.code` and couldn't be `switch`ed like every other SDK rejection.
23
+ They now reject with `CompanionError` and three new client-synthesized
24
+ codes: `call_unsupported` (non-browser environment — no WebRTC/mic),
25
+ `call_connect_failed` (mic-permission timeout, SDP exchange failed or
26
+ timed out; `status` carries the HTTP status when the exchange answered
27
+ non-2xx), and `call_dependency_missing` (the optional
28
+ `@elevenlabs/client` peer dependency isn't installed). Like
29
+ `reply_timeout`/`stream_unauthorized` these are SDK-synthesized and
30
+ deliberately NOT part of the server-mirrored `COMPANION_ERROR_CODES`
31
+ HTTP vocabulary. Browser-native `getUserMedia` rejections (e.g.
32
+ `NotAllowedError` on permission denial) still propagate untouched.
33
+ `CompanionError` itself is unchanged and keeps its import path.
34
+
15
35
  ## [0.25.0] - 2026-07-11
16
36
 
17
37
  ### Added
package/README.md CHANGED
@@ -147,7 +147,13 @@ HTTP `code` vocabulary behind `CompanionError.code`, e.g. `missing_scope` /
147
147
  `session_not_found` / `turn_pending`) — so you can assert the version, validate
148
148
  the event vocabulary, or switch on error codes without hard-coding strings.
149
149
  Every helper populates `CompanionError.code` when the server names a cause
150
- (0.24.0 — previously only the POST-backed calls did). Every
150
+ (0.24.0 — previously only the POST-backed calls did). The SDK also synthesizes
151
+ a few client-side codes outside that HTTP vocabulary: `reply_timeout`
152
+ (awaitReply fallback gave up), `stream_unauthorized` (event-stream 401
153
+ exhausted reconnects), and — 0.26.0 — the voice connect-step codes
154
+ `call_unsupported` (no WebRTC/mic in this environment), `call_connect_failed`
155
+ (mic timeout / SDP exchange failed) and `call_dependency_missing`
156
+ (`@elevenlabs/client` not installed). Every
151
157
  outbound payload has a named type; `ToolCallPayload` (`{ id, name, args }`) types
152
158
  the `companion.tool_call` event, and `onToolCall`'s callback receives
153
159
  `ToolCallEvent` — the payload plus `argsJson` (pre-parsed args).
package/dist/call.js CHANGED
@@ -10,6 +10,7 @@
10
10
  //
11
11
  // Browser-only (needs WebRTC + getUserMedia). Returns a handle to hang up and to
12
12
  // inject live context mid-call (the bridge the server uses for voiceRelevant).
13
+ import { CompanionError } from './errors.js';
13
14
  /** Universal "host control" verbs the companion can call on ANY surface (web /
14
15
  * game / app / hardware), regardless of provider. They're generic on purpose —
15
16
  * the SPECIFIC intent rides in the string params, which YOUR surface interprets
@@ -147,7 +148,7 @@ const OPENAI_REALTIME_CALLS = 'https://api.openai.com/v1/realtime/calls';
147
148
  /** Open a live call from pre-minted credentials. */
148
149
  export async function openCompanionCall(creds, opts = {}, bridge) {
149
150
  if (typeof navigator === 'undefined' || !navigator.mediaDevices) {
150
- throw new Error('connectCall is browser-only (needs WebRTC + microphone access)');
151
+ throw new CompanionError('connectCall is browser-only (needs WebRTC + microphone access)', 0, 'call_unsupported');
151
152
  }
152
153
  return creds.provider === 'openai-realtime'
153
154
  ? openOpenAICall(creds, opts, bridge)
@@ -176,7 +177,7 @@ async function getMicWithTimeout() {
176
177
  .catch(() => { });
177
178
  const timeout = new Promise((_, reject) => setTimeout(() => {
178
179
  timedOut = true;
179
- reject(new Error('microphone request timed out'));
180
+ reject(new CompanionError('microphone request timed out', 0, 'call_connect_failed'));
180
181
  }, GUM_TIMEOUT_MS));
181
182
  return Promise.race([micPromise, timeout]);
182
183
  }
@@ -343,14 +344,14 @@ async function openOpenAICall(creds, opts, bridge) {
343
344
  catch {
344
345
  clearTimeout(sdpTimer);
345
346
  cleanupPartial();
346
- throw new Error(sdpAbort.signal.aborted
347
+ throw new CompanionError(sdpAbort.signal.aborted
347
348
  ? 'OpenAI Realtime SDP exchange timed out'
348
- : 'OpenAI Realtime SDP exchange failed');
349
+ : 'OpenAI Realtime SDP exchange failed', 0, 'call_connect_failed');
349
350
  }
350
351
  clearTimeout(sdpTimer);
351
352
  if (!res.ok) {
352
353
  cleanupPartial();
353
- throw new Error(`OpenAI Realtime SDP exchange failed (${res.status})`);
354
+ throw new CompanionError(`OpenAI Realtime SDP exchange failed (${res.status})`, res.status, 'call_connect_failed');
354
355
  }
355
356
  await pc.setRemoteDescription({ type: 'answer', sdp: await res.text() });
356
357
  let closed = false;
@@ -458,7 +459,7 @@ async function openConvaiCall(creds, opts, bridge) {
458
459
  mod = await import('@elevenlabs/client');
459
460
  }
460
461
  catch {
461
- throw new Error("elevenlabs-convai call needs the optional peer dependency '@elevenlabs/client' — install it (npm i @elevenlabs/client)");
462
+ throw new CompanionError("elevenlabs-convai call needs the optional peer dependency '@elevenlabs/client' — install it (npm i @elevenlabs/client)", 0, 'call_dependency_missing');
462
463
  }
463
464
  // App tools → EL client tools. NOTE: ElevenLabs only CALLS a client tool the
464
465
  // agent already knows (declared on the agent config); a shared Convai agent
package/dist/client.d.ts CHANGED
@@ -166,12 +166,10 @@ export declare function pouchyBrandIconUrl(baseUrl: string, size?: BrandIconSize
166
166
  * a method before `connect()`). `code` is a stable machine-readable tag you can
167
167
  * switch on, shared with the `control.error` stream event vocabulary:
168
168
  * `'not_connected'`, `'missing_option'`, `'not_representative'`, or the server's
169
- * own error code for an HTTP failure (when it sends one). */
170
- export declare class CompanionError extends Error {
171
- status: number;
172
- code?: string;
173
- constructor(message: string, status: number, code?: string);
174
- }
169
+ * own error code for an HTTP failure (when it sends one). Defined in errors.ts
170
+ * (so call.ts shares it cycle-free); re-exported here so the public import
171
+ * path is unchanged. */
172
+ export { CompanionError } from './errors.js';
175
173
  /** The server's memory-consolidation diagnostic from `endSession()` / `close()`.
176
174
  * `ok` is whether consolidation ran; `facts` is how many memories it wrote;
177
175
  * `skipped` (when present) says why nothing was written
@@ -581,4 +579,3 @@ export declare class CompanionClient {
581
579
  private consume;
582
580
  private sleep;
583
581
  }
584
- export {};
package/dist/client.js CHANGED
@@ -14,6 +14,7 @@
14
14
  // kept in lockstep by protocol.drift.test.ts.
15
15
  import { PROTOCOL_VERSION } from './protocol.js';
16
16
  import { parseSse } from './sse.js';
17
+ import { CompanionError } from './errors.js';
17
18
  import { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_VISUAL_TOOLS, AVATAR_VISUAL_TOOL_NAMES } from './call.js';
18
19
  /** Model tool arguments are model output — tolerate junk without throwing. */
19
20
  function parseArgsJson(args) {
@@ -37,18 +38,10 @@ export function pouchyBrandIconUrl(baseUrl, size = 512) {
37
38
  * a method before `connect()`). `code` is a stable machine-readable tag you can
38
39
  * switch on, shared with the `control.error` stream event vocabulary:
39
40
  * `'not_connected'`, `'missing_option'`, `'not_representative'`, or the server's
40
- * own error code for an HTTP failure (when it sends one). */
41
- export class CompanionError extends Error {
42
- status;
43
- code;
44
- constructor(message, status, code) {
45
- super(message);
46
- this.name = 'CompanionError';
47
- this.status = status;
48
- if (code)
49
- this.code = code;
50
- }
51
- }
41
+ * own error code for an HTTP failure (when it sends one). Defined in errors.ts
42
+ * (so call.ts shares it cycle-free); re-exported here so the public import
43
+ * path is unchanged. */
44
+ export { CompanionError } from './errors.js';
52
45
  const SEEN_CAP = 512;
53
46
  export class CompanionClient {
54
47
  opts;
@@ -0,0 +1,9 @@
1
+ /** Thrown by every helper on an HTTP or client-side failure. `status` is the
2
+ * HTTP status (0 for client-synthesized failures — timeouts, unsupported
3
+ * environments, missing optional deps); `code` is machine-switchable when
4
+ * the server (or the SDK itself) names a cause. */
5
+ export declare class CompanionError extends Error {
6
+ status: number;
7
+ code?: string;
8
+ constructor(message: string, status: number, code?: string);
9
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,20 @@
1
+ // CompanionError — the one error type every SDK helper rejects with (README
2
+ // contract: consumers `switch` on `.code` instead of string-matching prose).
3
+ // Lives in its own module so call.ts can use it without a runtime cycle
4
+ // through client.ts (client.ts re-exports it, keeping the public import path
5
+ // `@pouchy_ai/companion-sdk` → `CompanionError` unchanged).
6
+ /** Thrown by every helper on an HTTP or client-side failure. `status` is the
7
+ * HTTP status (0 for client-synthesized failures — timeouts, unsupported
8
+ * environments, missing optional deps); `code` is machine-switchable when
9
+ * the server (or the SDK itself) names a cause. */
10
+ export class CompanionError extends Error {
11
+ status;
12
+ code;
13
+ constructor(message, status, code) {
14
+ super(message);
15
+ this.name = 'CompanionError';
16
+ this.status = status;
17
+ if (code)
18
+ this.code = code;
19
+ }
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/companion-sdk",
3
- "version": "0.25.0",
3
+ "version": "0.26.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",