@pouchy_ai/companion-sdk 0.28.0 → 0.29.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 +72 -1
- package/README.md +19 -0
- package/dist/call.js +20 -6
- package/dist/client.d.ts +42 -9
- package/dist/client.js +132 -37
- package/dist/errors.d.ts +13 -4
- package/dist/errors.js +14 -0
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,59 @@ a protocol bump is always called out explicitly here.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.29.0] - 2026-07-12
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`AbortSignal` on every network method.** All request-performing public
|
|
20
|
+
methods now accept an optional `signal` — cancel an in-flight `sendText`
|
|
21
|
+
(including its `awaitReply` deadline + event-stream teardown), `recall`,
|
|
22
|
+
`history`, `sendWorldState`, `ingestKnowledge`, `ingestFile`, `remember`,
|
|
23
|
+
`getAvatar`, `getWallet`, `confirmAction`, `pendingConfirms`,
|
|
24
|
+
`setModalities`, `ping`, `pairVisitor`, or `connect`. Methods with an
|
|
25
|
+
existing options object gained a `signal?` field; the rest gained a trailing
|
|
26
|
+
`opts?: { signal?: AbortSignal }`. Fully additive — no existing call
|
|
27
|
+
changes. A fired signal rejects with `CompanionError` `code: 'aborted'`
|
|
28
|
+
(new synthesized code), converted at the single fetch choke point so the
|
|
29
|
+
"every helper rejects with CompanionError" contract holds; the signal also
|
|
30
|
+
rides the 401-refresh retry automatically.
|
|
31
|
+
|
|
32
|
+
## [0.28.1] - 2026-07-12
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **WebSocket reconnect storm on short-lived connections.** A gateway that
|
|
37
|
+
accepted the socket and immediately closed it cleanly (auth-expressed-as-
|
|
38
|
+
close, LB idle policy, misconfigured proxy) made `stream: 'websocket'`
|
|
39
|
+
reconnect in a zero-delay hot loop. A connection that lived under ~2s now
|
|
40
|
+
sits out the same 500ms→8s doubling backoff the SSE loop uses (a long-lived
|
|
41
|
+
connection still reconnects immediately).
|
|
42
|
+
- **A throwing event handler no longer poisons the stream loop.** Handler
|
|
43
|
+
exceptions used to propagate into the SSE/WS loop's catch — read as a
|
|
44
|
+
CONNECTION failure (backoff + reconnect + cursor replay) — and, on the
|
|
45
|
+
`sendText` local-emit path, rejected a `sendText` whose turn had actually
|
|
46
|
+
succeeded. Handlers are now isolated per-call (logged, remaining handlers
|
|
47
|
+
still run).
|
|
48
|
+
- **Custom voice-tool bridges may throw.** With `openCompanionCall` and a
|
|
49
|
+
bring-your-own bridge whose `invoke` rejected, the model never received a
|
|
50
|
+
`function_call_output` — the voice turn hung until the provider timeout,
|
|
51
|
+
plus an unhandled rejection. A rejecting bridge now answers the model with
|
|
52
|
+
`{ ok: false, error }`.
|
|
53
|
+
- Timer hygiene: the 20s voice-tool timeout and the 10s microphone-acquire
|
|
54
|
+
timeout are now cleared on normal completion instead of lingering.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- **`CompanionErrorCodeValue` is now DERIVED** from the drift-tested server
|
|
59
|
+
vocabulary plus the SDK-synthesized list, instead of hand-retyped — the old
|
|
60
|
+
copy had silently drifted 5 codes behind (`not_representative`,
|
|
61
|
+
`call_unsupported`, `call_connect_failed`, `call_dependency_missing` now
|
|
62
|
+
autocomplete). A new drift test pins every `new CompanionError(...)` call
|
|
63
|
+
site to a declared code. Types-only; no runtime behavior change.
|
|
64
|
+
- `package.json` now declares `"engines": { "node": ">=18" }` and the README
|
|
65
|
+
documents that the package ships **ESM-only** (modern bundlers and Node ≥ 18
|
|
66
|
+
consume it as-is; there is no CJS build).
|
|
67
|
+
|
|
15
68
|
## [0.28.0] - 2026-07-12
|
|
16
69
|
|
|
17
70
|
### Added
|
|
@@ -563,7 +616,25 @@ self-hosted/CDN build; this entry captures the surface that ships publicly.
|
|
|
563
616
|
- **Versioned wire protocol.** `PROTOCOL_VERSION = 1`, kept in lockstep with the
|
|
564
617
|
server by `protocol.drift.test.ts` (fails CI on divergence).
|
|
565
618
|
|
|
566
|
-
[Unreleased]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.
|
|
619
|
+
[Unreleased]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.29.0...HEAD
|
|
620
|
+
[0.29.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.28.1...companion-sdk-v0.29.0
|
|
621
|
+
[0.28.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.28.0...companion-sdk-v0.28.1
|
|
622
|
+
[0.28.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.27.0...companion-sdk-v0.28.0
|
|
623
|
+
[0.27.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.26.1...companion-sdk-v0.27.0
|
|
624
|
+
[0.26.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.26.0...companion-sdk-v0.26.1
|
|
625
|
+
[0.26.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.25.0...companion-sdk-v0.26.0
|
|
626
|
+
[0.25.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.24.0...companion-sdk-v0.25.0
|
|
627
|
+
[0.24.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.23.0...companion-sdk-v0.24.0
|
|
628
|
+
[0.23.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.22.0...companion-sdk-v0.23.0
|
|
629
|
+
[0.22.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.21.0...companion-sdk-v0.22.0
|
|
630
|
+
[0.21.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.20.0...companion-sdk-v0.21.0
|
|
631
|
+
[0.20.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.19.1...companion-sdk-v0.20.0
|
|
632
|
+
[0.19.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.19.0...companion-sdk-v0.19.1
|
|
633
|
+
[0.19.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.18.1...companion-sdk-v0.19.0
|
|
634
|
+
[0.18.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.18.0...companion-sdk-v0.18.1
|
|
635
|
+
[0.18.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.17.0...companion-sdk-v0.18.0
|
|
636
|
+
[0.17.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.13.0...companion-sdk-v0.17.0
|
|
637
|
+
[0.13.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.12.1...companion-sdk-v0.13.0
|
|
567
638
|
[0.12.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.12.0...companion-sdk-v0.12.1
|
|
568
639
|
[0.12.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.11.0...companion-sdk-v0.12.0
|
|
569
640
|
[0.11.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.10.1...companion-sdk-v0.11.0
|
package/README.md
CHANGED
|
@@ -16,6 +16,10 @@ npm i @pouchy_ai/companion-sdk
|
|
|
16
16
|
|
|
17
17
|
No bundler? Pull it from a CDN via an import map instead — see
|
|
18
18
|
[`docs/companion-integration-faq.md`](../../docs/companion-integration-faq.md).
|
|
19
|
+
|
|
20
|
+
The package ships **ESM-only** (`"type": "module"`, no CJS build). Every modern
|
|
21
|
+
bundler and Node ≥ 18 consumes it as-is; on Node ≥ 20.17 even `require()` of the
|
|
22
|
+
ESM entry works natively.
|
|
19
23
|
Changes per release are tracked in [`CHANGELOG.md`](./CHANGELOG.md).
|
|
20
24
|
|
|
21
25
|
**React?** [`@pouchy_ai/react`](../react-sdk/README.md) wraps this SDK in a
|
|
@@ -76,6 +80,7 @@ Subscribe with `on(type, fn)` (or `'*'`), or these typed convenience helpers:
|
|
|
76
80
|
| `onExpression(fn)` | `companion.expression` | avatar viseme / expression / gesture. _(reserved — not emitted yet)_ |
|
|
77
81
|
| `onVoiceInject(fn)` | `companion.voice_inject` | `fn({text, speak})` — a `voiceRelevant` world-state line to say aloud during a live call; route `text` to your voice session when `speak` |
|
|
78
82
|
| `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
|
+
| `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 |
|
|
79
84
|
| `onUsage(fn)` | `control.usage` | per-token metering echo. _(reserved — not emitted yet)_ |
|
|
80
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) |
|
|
81
86
|
|
|
@@ -117,6 +122,20 @@ are offered automatically once you declare `tools`/`handles`, and
|
|
|
117
122
|
embeds receive expression cues — declare them or they no-op silently. Full
|
|
118
123
|
contract: `docs/companion-host-control.md`.
|
|
119
124
|
|
|
125
|
+
**Cancellation (0.29.0):** every request-performing method accepts an optional
|
|
126
|
+
`AbortSignal`. Methods with an options object take a `signal?` field
|
|
127
|
+
(`sendText(text, { signal })`, `recall({ signal })`, …); the rest take a
|
|
128
|
+
trailing `opts` (`getAvatar({ signal })`, `ping({ signal })`, …). Aborting
|
|
129
|
+
rejects with `CompanionError` `code: 'aborted'`, and for `sendText({ awaitReply })`
|
|
130
|
+
it also tears down the in-flight event stream.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const ac = new AbortController();
|
|
134
|
+
const p = companion.sendText('summarize this', { awaitReply: true, signal: ac.signal });
|
|
135
|
+
// …user navigated away
|
|
136
|
+
ac.abort(); // p rejects with code: 'aborted'
|
|
137
|
+
```
|
|
138
|
+
|
|
120
139
|
## Scopes (typed)
|
|
121
140
|
|
|
122
141
|
The full capability-scope vocabulary ships as typed constants — no more
|
package/dist/call.js
CHANGED
|
@@ -175,11 +175,18 @@ async function getMicWithTimeout() {
|
|
|
175
175
|
m.getTracks().forEach((t) => t.stop());
|
|
176
176
|
})
|
|
177
177
|
.catch(() => { });
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
let timer;
|
|
179
|
+
const timeout = new Promise((_, reject) => {
|
|
180
|
+
timer = setTimeout(() => {
|
|
181
|
+
timedOut = true;
|
|
182
|
+
reject(new CompanionError('microphone request timed out', 0, 'call_connect_failed'));
|
|
183
|
+
}, GUM_TIMEOUT_MS);
|
|
184
|
+
});
|
|
185
|
+
// Clear the timer when the mic wins the race — it held a live 10s closure.
|
|
186
|
+
return Promise.race([micPromise, timeout]).finally(() => {
|
|
187
|
+
if (timer)
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
});
|
|
183
190
|
}
|
|
184
191
|
// ── OpenAI Realtime — self-contained WebRTC ─────────────────────────────────
|
|
185
192
|
async function openOpenAICall(creds, opts, bridge) {
|
|
@@ -289,7 +296,14 @@ async function openOpenAICall(creds, opts, bridge) {
|
|
|
289
296
|
callNames.delete(callId);
|
|
290
297
|
if (name && bridge.tools.some((t) => t.name === name)) {
|
|
291
298
|
const argsText = typeof msg.arguments === 'string' ? msg.arguments : '{}';
|
|
292
|
-
|
|
299
|
+
// invokeVoiceTool never rejects, but openCompanionCall is a public
|
|
300
|
+
// export (bring-your-own bridge) — a throwing consumer bridge must
|
|
301
|
+
// still answer the model, or the voice turn hangs until provider
|
|
302
|
+
// timeout (plus an unhandled rejection).
|
|
303
|
+
void bridge
|
|
304
|
+
.invoke(name, argsText)
|
|
305
|
+
.catch((e) => JSON.stringify({ ok: false, error: e instanceof Error ? e.message : 'tool failed' }))
|
|
306
|
+
.then((output) => {
|
|
293
307
|
try {
|
|
294
308
|
dc.send(JSON.stringify({
|
|
295
309
|
type: 'conversation.item.create',
|
package/dist/client.d.ts
CHANGED
|
@@ -230,6 +230,11 @@ export declare class CompanionClient {
|
|
|
230
230
|
* the strict awaitReply correlation is only safe to enforce there. */
|
|
231
231
|
private serverEchoesReplyTo;
|
|
232
232
|
private fetchAuthed;
|
|
233
|
+
/** Normalize a thrown fetch error to a CompanionError. An AbortError (the
|
|
234
|
+
* caller's signal fired) becomes a switchable `code: 'aborted'`; anything
|
|
235
|
+
* else that is already a CompanionError passes through; the rest wrap as a
|
|
236
|
+
* status-0 network failure. */
|
|
237
|
+
private asCompanionError;
|
|
233
238
|
/** Peek the error `code` of a failure response without consuming its body. */
|
|
234
239
|
private errorCodeOf;
|
|
235
240
|
private url;
|
|
@@ -241,7 +246,9 @@ export declare class CompanionClient {
|
|
|
241
246
|
private retryAfterFrom;
|
|
242
247
|
private postJson;
|
|
243
248
|
/** Handshake: start or resume the session for this surface. */
|
|
244
|
-
connect(
|
|
249
|
+
connect(opts?: {
|
|
250
|
+
signal?: AbortSignal;
|
|
251
|
+
}): Promise<HelloAck>;
|
|
245
252
|
/** Pair this representative session's VISITOR with the owner so their two
|
|
246
253
|
* companions become friends — unlocking the A2A plane (messaging, gifts,
|
|
247
254
|
* visiting) between them. Only valid in a representative session (the client
|
|
@@ -250,7 +257,9 @@ export declare class CompanionClient {
|
|
|
250
257
|
* Two-token consent: this client's PAT (the owner's) must hold `represent:pair`,
|
|
251
258
|
* and you pass the VISITOR's own Pouchy PAT — which must hold `social.message`
|
|
252
259
|
* — as proof + consent. The visitor must therefore also be a Pouchy user. */
|
|
253
|
-
pairVisitor(visitorToken: string
|
|
260
|
+
pairVisitor(visitorToken: string, opts?: {
|
|
261
|
+
signal?: AbortSignal;
|
|
262
|
+
}): Promise<{
|
|
254
263
|
pairId: string | null;
|
|
255
264
|
}>;
|
|
256
265
|
/** Send a user text turn, optionally with images (data URLs) for a multimodal
|
|
@@ -279,12 +288,14 @@ export declare class CompanionClient {
|
|
|
279
288
|
images?: string[];
|
|
280
289
|
stream?: boolean;
|
|
281
290
|
replyTimeoutMs?: number;
|
|
291
|
+
signal?: AbortSignal;
|
|
282
292
|
}): Promise<SendTextReply>;
|
|
283
293
|
sendText(text: string, opts?: {
|
|
284
294
|
images?: string[];
|
|
285
295
|
stream?: boolean;
|
|
286
296
|
awaitReply?: boolean;
|
|
287
297
|
replyTimeoutMs?: number;
|
|
298
|
+
signal?: AbortSignal;
|
|
288
299
|
}): Promise<{
|
|
289
300
|
seq: number | null;
|
|
290
301
|
}>;
|
|
@@ -312,7 +323,9 @@ export declare class CompanionClient {
|
|
|
312
323
|
* (id → a fresh handle, source → this surface), so callers only supply the
|
|
313
324
|
* meaningful fields: `{ type, data, retained?, salience?, voiceRelevant? }`.
|
|
314
325
|
* A full CloudEvents envelope is still accepted (its fields win). */
|
|
315
|
-
sendWorldState(event: WorldStateInput | WorldStateInput[]
|
|
326
|
+
sendWorldState(event: WorldStateInput | WorldStateInput[], opts?: {
|
|
327
|
+
signal?: AbortSignal;
|
|
328
|
+
}): Promise<{
|
|
316
329
|
accepted: number;
|
|
317
330
|
dropped: number;
|
|
318
331
|
injected: number;
|
|
@@ -404,6 +417,7 @@ export declare class CompanionClient {
|
|
|
404
417
|
recall(opts?: {
|
|
405
418
|
limit?: number;
|
|
406
419
|
query?: string;
|
|
420
|
+
signal?: AbortSignal;
|
|
407
421
|
}): Promise<RecalledMemory[]>;
|
|
408
422
|
/** Fetch this session's recent conversation turns (oldest→newest) so a
|
|
409
423
|
* reconnecting embed can restore its transcript. Distinct from `recall`
|
|
@@ -412,18 +426,23 @@ export declare class CompanionClient {
|
|
|
412
426
|
* first). `limit` defaults to 20, capped at 50. */
|
|
413
427
|
history(opts?: {
|
|
414
428
|
limit?: number;
|
|
429
|
+
signal?: AbortSignal;
|
|
415
430
|
}): Promise<CompanionTurn[]>;
|
|
416
431
|
/** Change this session's I/O modalities mid-session (e.g. enable/disable
|
|
417
432
|
* `voice`). The request is intersected with the token's granted modalities
|
|
418
433
|
* server-side — a session can't widen past its key — and the EFFECTIVE set is
|
|
419
434
|
* returned. Requires a live session. */
|
|
420
|
-
setModalities(modalities: string[]
|
|
435
|
+
setModalities(modalities: string[], opts?: {
|
|
436
|
+
signal?: AbortSignal;
|
|
437
|
+
}): Promise<{
|
|
421
438
|
modalities: string[];
|
|
422
439
|
}>;
|
|
423
440
|
/** Keepalive: bump this session's last-seen time so a long-idle embed stays
|
|
424
441
|
* "live" within the session TTL (cross-app A2A friend messages keep reaching
|
|
425
442
|
* it). Cheap; call it on a timer for a background tab. Requires a live session. */
|
|
426
|
-
ping(
|
|
443
|
+
ping(opts?: {
|
|
444
|
+
signal?: AbortSignal;
|
|
445
|
+
}): Promise<void>;
|
|
427
446
|
/** Remember a fact (into this app's namespace unless `namespace` is given and
|
|
428
447
|
* the token has the core scope). Intimate-tier writes are rejected server-side. */
|
|
429
448
|
remember(fact: {
|
|
@@ -436,6 +455,8 @@ export declare class CompanionClient {
|
|
|
436
455
|
kind?: string;
|
|
437
456
|
namespace?: string;
|
|
438
457
|
sensitivity?: 'public' | 'personal';
|
|
458
|
+
}, opts?: {
|
|
459
|
+
signal?: AbortSignal;
|
|
439
460
|
}): Promise<{
|
|
440
461
|
cloudId: string;
|
|
441
462
|
namespace: string;
|
|
@@ -459,6 +480,8 @@ export declare class CompanionClient {
|
|
|
459
480
|
name: string;
|
|
460
481
|
kind?: string;
|
|
461
482
|
locale?: string;
|
|
483
|
+
}, opts?: {
|
|
484
|
+
signal?: AbortSignal;
|
|
462
485
|
}): Promise<{
|
|
463
486
|
ok: boolean;
|
|
464
487
|
summary: string;
|
|
@@ -477,6 +500,8 @@ export declare class CompanionClient {
|
|
|
477
500
|
name: string;
|
|
478
501
|
kind?: string;
|
|
479
502
|
locale?: string;
|
|
503
|
+
}, opts?: {
|
|
504
|
+
signal?: AbortSignal;
|
|
480
505
|
}): Promise<{
|
|
481
506
|
ok: boolean;
|
|
482
507
|
summary: string;
|
|
@@ -488,13 +513,17 @@ export declare class CompanionClient {
|
|
|
488
513
|
* portrait when one exists (null today for built-in models). URLs are absolute
|
|
489
514
|
* and the /models asset is CORS-enabled, so a cross-origin renderer can fetch
|
|
490
515
|
* the VRM directly. Reflects the user's live choice (built-in or custom). */
|
|
491
|
-
getAvatar(
|
|
516
|
+
getAvatar(opts?: {
|
|
517
|
+
signal?: AbortSignal;
|
|
518
|
+
}): Promise<CompanionAvatar>;
|
|
492
519
|
/** Read the companion instance's OWN wallet — nonzero stablecoin balances +
|
|
493
520
|
* total USD. Read-only and receive-only (spends go through the confirm-gated
|
|
494
521
|
* pay tools, never here). Requires the `wallet.read` scope (or `wallet.spend`,
|
|
495
522
|
* which subsumes it). Same data the companion gives when asked "what's my
|
|
496
523
|
* balance?". */
|
|
497
|
-
getWallet(
|
|
524
|
+
getWallet(opts?: {
|
|
525
|
+
signal?: AbortSignal;
|
|
526
|
+
}): Promise<CompanionWallet>;
|
|
498
527
|
/** Canonical URL of the official Pouchy brand icon (square PNG, transparent
|
|
499
528
|
* background) at the given size. Static + public — needs no token — so it's
|
|
500
529
|
* safe to drop straight into an `<img src>` for "powered by Pouchy" badges,
|
|
@@ -549,7 +578,9 @@ export declare class CompanionClient {
|
|
|
549
578
|
* MAY call `confirmAction(sameConfirmId, true)` again to re-run it (the server
|
|
550
579
|
* dedupes a partial first attempt). A non-idempotent action (a message / skill)
|
|
551
580
|
* stays terminal and is never retryable. */
|
|
552
|
-
confirmAction(confirmId: string, approve: boolean
|
|
581
|
+
confirmAction(confirmId: string, approve: boolean, opts?: {
|
|
582
|
+
signal?: AbortSignal;
|
|
583
|
+
}): Promise<{
|
|
553
584
|
ok: boolean;
|
|
554
585
|
status: 'approved' | 'denied' | 'exec_failed';
|
|
555
586
|
outcome?: string;
|
|
@@ -559,7 +590,9 @@ export declare class CompanionClient {
|
|
|
559
590
|
* scope, timestamps — never raw args). Use it to rebuild your confirm card
|
|
560
591
|
* after a reload, since confirm_request events are not replayed. Platform
|
|
561
592
|
* session tokens only, like `confirmAction`. */
|
|
562
|
-
pendingConfirms(
|
|
593
|
+
pendingConfirms(opts?: {
|
|
594
|
+
signal?: AbortSignal;
|
|
595
|
+
}): Promise<PendingConfirm[]>;
|
|
563
596
|
/** Convenience: subscribe to live updates of an already-rendered Instant UI
|
|
564
597
|
* panel (companion.ui_update). Apply each `{ key, value }` to the panel's state
|
|
565
598
|
* bag and re-evaluate bound displays — no rebuild. */
|
package/dist/client.js
CHANGED
|
@@ -120,7 +120,16 @@ export class CompanionClient {
|
|
|
120
120
|
return major > 1 || (major === 1 && minor >= 1);
|
|
121
121
|
}
|
|
122
122
|
async fetchAuthed(url, init) {
|
|
123
|
-
|
|
123
|
+
let res;
|
|
124
|
+
try {
|
|
125
|
+
res = await this.doFetch(url, init);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
// A caller-supplied AbortSignal makes fetch throw a native AbortError —
|
|
129
|
+
// convert it at this single choke point so cancellation honors the
|
|
130
|
+
// "every helper rejects with CompanionError" contract (code 'aborted').
|
|
131
|
+
throw this.asCompanionError(e);
|
|
132
|
+
}
|
|
124
133
|
const apiVersion = res.headers.get('x-pouchy-api-version');
|
|
125
134
|
if (apiVersion)
|
|
126
135
|
this.serverApiVersion = apiVersion;
|
|
@@ -135,7 +144,24 @@ export class CompanionClient {
|
|
|
135
144
|
if (!(await this.tryRefreshToken()))
|
|
136
145
|
return res;
|
|
137
146
|
init.headers.Authorization = `Bearer ${this.opts.token}`;
|
|
138
|
-
|
|
147
|
+
try {
|
|
148
|
+
return await this.doFetch(url, init);
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
throw this.asCompanionError(e);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Normalize a thrown fetch error to a CompanionError. An AbortError (the
|
|
155
|
+
* caller's signal fired) becomes a switchable `code: 'aborted'`; anything
|
|
156
|
+
* else that is already a CompanionError passes through; the rest wrap as a
|
|
157
|
+
* status-0 network failure. */
|
|
158
|
+
asCompanionError(e) {
|
|
159
|
+
if (e instanceof CompanionError)
|
|
160
|
+
return e;
|
|
161
|
+
const name = e?.name;
|
|
162
|
+
if (name === 'AbortError')
|
|
163
|
+
return new CompanionError('request aborted', 0, 'aborted');
|
|
164
|
+
return new CompanionError(e instanceof Error ? e.message : 'network request failed', 0);
|
|
139
165
|
}
|
|
140
166
|
/** Peek the error `code` of a failure response without consuming its body. */
|
|
141
167
|
async errorCodeOf(res) {
|
|
@@ -168,11 +194,14 @@ export class CompanionClient {
|
|
|
168
194
|
const header = Number(res.headers.get('Retry-After'));
|
|
169
195
|
return Number.isFinite(header) && header >= 0 ? header : undefined;
|
|
170
196
|
}
|
|
171
|
-
async postJson(path, body) {
|
|
197
|
+
async postJson(path, body, signal) {
|
|
172
198
|
const res = await this.fetchAuthed(this.url(path), {
|
|
173
199
|
method: 'POST',
|
|
174
200
|
headers: this.jsonHeaders(),
|
|
175
|
-
body: JSON.stringify(body)
|
|
201
|
+
body: JSON.stringify(body),
|
|
202
|
+
// The signal rides `init`, so the 401-refresh retry in fetchAuthed
|
|
203
|
+
// reuses it automatically (init is passed to both doFetch calls).
|
|
204
|
+
...(signal ? { signal } : {})
|
|
176
205
|
});
|
|
177
206
|
const json = (await res.json().catch(() => null));
|
|
178
207
|
if (!res.ok || !json || json.ok === false) {
|
|
@@ -185,7 +214,7 @@ export class CompanionClient {
|
|
|
185
214
|
return json;
|
|
186
215
|
}
|
|
187
216
|
/** Handshake: start or resume the session for this surface. */
|
|
188
|
-
async connect() {
|
|
217
|
+
async connect(opts) {
|
|
189
218
|
const json = await this.postJson('/api/companion/session', {
|
|
190
219
|
surface: this.opts.surface ?? 'default',
|
|
191
220
|
modalities: this.opts.modalities,
|
|
@@ -194,7 +223,7 @@ export class CompanionClient {
|
|
|
194
223
|
tools: this.opts.tools,
|
|
195
224
|
appContext: this.opts.appContext,
|
|
196
225
|
...(this.opts.visitor ? { visitor: this.opts.visitor } : {})
|
|
197
|
-
});
|
|
226
|
+
}, opts?.signal);
|
|
198
227
|
this._session = json.session;
|
|
199
228
|
this.cursor = json.resumeCursor ?? 0;
|
|
200
229
|
return {
|
|
@@ -211,7 +240,7 @@ export class CompanionClient {
|
|
|
211
240
|
* Two-token consent: this client's PAT (the owner's) must hold `represent:pair`,
|
|
212
241
|
* and you pass the VISITOR's own Pouchy PAT — which must hold `social.message`
|
|
213
242
|
* — as proof + consent. The visitor must therefore also be a Pouchy user. */
|
|
214
|
-
async pairVisitor(visitorToken) {
|
|
243
|
+
async pairVisitor(visitorToken, opts) {
|
|
215
244
|
if (!this.opts.visitor) {
|
|
216
245
|
throw new CompanionError('pairVisitor() requires a representative session (createCompanion({ visitor }))', 0, 'not_representative');
|
|
217
246
|
}
|
|
@@ -220,7 +249,7 @@ export class CompanionClient {
|
|
|
220
249
|
const json = await this.postJson('/api/companion/pair', {
|
|
221
250
|
visitorToken,
|
|
222
251
|
visitorId: this.opts.visitor.id
|
|
223
|
-
});
|
|
252
|
+
}, opts?.signal);
|
|
224
253
|
return { pairId: json.pairId ?? null };
|
|
225
254
|
}
|
|
226
255
|
async sendText(text, opts) {
|
|
@@ -235,10 +264,10 @@ export class CompanionClient {
|
|
|
235
264
|
return this.sendTextAwaitingReply(id, body, opts, turnId);
|
|
236
265
|
const wantStream = opts?.stream ?? this.deltaHandlers.size > 0;
|
|
237
266
|
if (!wantStream) {
|
|
238
|
-
const json = await this.postJson(`/api/companion/session/${id}/input`, body);
|
|
267
|
+
const json = await this.postJson(`/api/companion/session/${id}/input`, body, opts?.signal);
|
|
239
268
|
return { seq: json.seq ?? null };
|
|
240
269
|
}
|
|
241
|
-
return this.sendTextStreaming(id, body);
|
|
270
|
+
return this.sendTextStreaming(id, body, opts?.signal);
|
|
242
271
|
}
|
|
243
272
|
/** The awaitReply leg of sendText. Prefers the in-request answer (the
|
|
244
273
|
* streaming `done` frame's envelope); falls back to the next
|
|
@@ -265,12 +294,27 @@ export class CompanionClient {
|
|
|
265
294
|
// promise past replyTimeoutMs (previously only the fallback wait was
|
|
266
295
|
// bounded). The abort tears the in-flight stream down with it.
|
|
267
296
|
const ac = new AbortController();
|
|
297
|
+
// Link the caller's signal: aborting it tears down the in-flight stream
|
|
298
|
+
// (via ac) AND rejects the whole operation with `aborted`, exactly like
|
|
299
|
+
// the deadline does. If it is already aborted, fail fast.
|
|
300
|
+
let onCallerAbort;
|
|
301
|
+
if (opts?.signal) {
|
|
302
|
+
if (opts.signal.aborted)
|
|
303
|
+
throw new CompanionError('request aborted', 0, 'aborted');
|
|
304
|
+
onCallerAbort = () => ac.abort();
|
|
305
|
+
opts.signal.addEventListener('abort', onCallerAbort, { once: true });
|
|
306
|
+
}
|
|
268
307
|
let timer;
|
|
269
308
|
const deadline = new Promise((_, reject) => {
|
|
270
309
|
timer = setTimeout(() => {
|
|
271
310
|
ac.abort();
|
|
272
311
|
reject(new CompanionError(`timed out after ${timeoutMs}ms waiting for the reply — buffered mode needs start() polling the event stream`, 0, 'reply_timeout'));
|
|
273
312
|
}, timeoutMs);
|
|
313
|
+
// The caller's signal also rejects the operation (the stream teardown
|
|
314
|
+
// alone would otherwise leave the fallback wait hanging to the timer).
|
|
315
|
+
if (opts?.signal) {
|
|
316
|
+
opts.signal.addEventListener('abort', () => reject(new CompanionError('request aborted', 0, 'aborted')), { once: true });
|
|
317
|
+
}
|
|
274
318
|
});
|
|
275
319
|
try {
|
|
276
320
|
let seq = null;
|
|
@@ -308,6 +352,8 @@ export class CompanionClient {
|
|
|
308
352
|
off();
|
|
309
353
|
if (timer !== undefined)
|
|
310
354
|
clearTimeout(timer);
|
|
355
|
+
if (onCallerAbort)
|
|
356
|
+
opts?.signal?.removeEventListener('abort', onCallerAbort);
|
|
311
357
|
}
|
|
312
358
|
}
|
|
313
359
|
/** The streaming leg of sendText: parse the POST response's SSE frames —
|
|
@@ -430,7 +476,7 @@ export class CompanionClient {
|
|
|
430
476
|
* (id → a fresh handle, source → this surface), so callers only supply the
|
|
431
477
|
* meaningful fields: `{ type, data, retained?, salience?, voiceRelevant? }`.
|
|
432
478
|
* A full CloudEvents envelope is still accepted (its fields win). */
|
|
433
|
-
async sendWorldState(event) {
|
|
479
|
+
async sendWorldState(event, opts) {
|
|
434
480
|
const id = this.requireSession();
|
|
435
481
|
const fill = (e) => ({
|
|
436
482
|
...e,
|
|
@@ -439,7 +485,7 @@ export class CompanionClient {
|
|
|
439
485
|
source: e.source ?? this.opts.surface ?? 'companion-sdk'
|
|
440
486
|
});
|
|
441
487
|
const body = Array.isArray(event) ? { events: event.map(fill) } : fill(event);
|
|
442
|
-
const json = await this.postJson(`/api/companion/session/${id}/context`, body);
|
|
488
|
+
const json = await this.postJson(`/api/companion/session/${id}/context`, body, opts?.signal);
|
|
443
489
|
// injected/reacted tell the host a live-call inject or a proactive text
|
|
444
490
|
// reaction was triggered by this batch (0.28.0 — previously discarded).
|
|
445
491
|
return {
|
|
@@ -674,15 +720,20 @@ export class CompanionClient {
|
|
|
674
720
|
const id = `vc_${this.randomId()}`;
|
|
675
721
|
return new Promise((resolve) => {
|
|
676
722
|
let settled = false;
|
|
723
|
+
let timer;
|
|
677
724
|
const finish = (out) => {
|
|
678
725
|
if (settled)
|
|
679
726
|
return;
|
|
680
727
|
settled = true;
|
|
728
|
+
// Clear on normal completion too — a chatty tool-using call was
|
|
729
|
+
// accumulating a live 20s timer (+closure) per invocation.
|
|
730
|
+
if (timer)
|
|
731
|
+
clearTimeout(timer);
|
|
681
732
|
this.pendingVoiceTools.delete(id);
|
|
682
733
|
resolve(out);
|
|
683
734
|
};
|
|
684
735
|
this.pendingVoiceTools.set(id, finish);
|
|
685
|
-
setTimeout(() => finish(JSON.stringify({ ok: false, error: 'tool result timed out' })), 20_000);
|
|
736
|
+
timer = setTimeout(() => finish(JSON.stringify({ ok: false, error: 'tool result timed out' })), 20_000);
|
|
686
737
|
this.emit({
|
|
687
738
|
v: PROTOCOL_VERSION,
|
|
688
739
|
id: `evt_${this.randomId()}`,
|
|
@@ -731,7 +782,8 @@ export class CompanionClient {
|
|
|
731
782
|
const query = params.toString();
|
|
732
783
|
const qs = query ? `?${query}` : '';
|
|
733
784
|
const res = await this.fetchAuthed(this.url(`/api/companion/memory${qs}`), {
|
|
734
|
-
headers: { Authorization: `Bearer ${this.opts.token}` }
|
|
785
|
+
headers: { Authorization: `Bearer ${this.opts.token}` },
|
|
786
|
+
...(opts?.signal ? { signal: opts.signal } : {})
|
|
735
787
|
});
|
|
736
788
|
const json = (await res.json().catch(() => null));
|
|
737
789
|
if (!res.ok || !json || json.ok === false) {
|
|
@@ -747,7 +799,10 @@ export class CompanionClient {
|
|
|
747
799
|
async history(opts) {
|
|
748
800
|
const id = this.requireSession();
|
|
749
801
|
const qs = opts?.limit ? `?limit=${encodeURIComponent(opts.limit)}` : '';
|
|
750
|
-
const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/history${qs}`), {
|
|
802
|
+
const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/history${qs}`), {
|
|
803
|
+
headers: { Authorization: `Bearer ${this.opts.token}` },
|
|
804
|
+
...(opts?.signal ? { signal: opts.signal } : {})
|
|
805
|
+
});
|
|
751
806
|
const json = (await res.json().catch(() => null));
|
|
752
807
|
if (!res.ok || !json || json.ok === false) {
|
|
753
808
|
throw new CompanionError(json?.error || `history failed (${res.status})`, res.status, json?.code);
|
|
@@ -758,7 +813,7 @@ export class CompanionClient {
|
|
|
758
813
|
* `voice`). The request is intersected with the token's granted modalities
|
|
759
814
|
* server-side — a session can't widen past its key — and the EFFECTIVE set is
|
|
760
815
|
* returned. Requires a live session. */
|
|
761
|
-
async setModalities(modalities) {
|
|
816
|
+
async setModalities(modalities, opts) {
|
|
762
817
|
const id = this.requireSession();
|
|
763
818
|
const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/modalities`), {
|
|
764
819
|
method: 'POST',
|
|
@@ -766,7 +821,8 @@ export class CompanionClient {
|
|
|
766
821
|
Authorization: `Bearer ${this.opts.token}`,
|
|
767
822
|
'Content-Type': 'application/json'
|
|
768
823
|
},
|
|
769
|
-
body: JSON.stringify({ modalities })
|
|
824
|
+
body: JSON.stringify({ modalities }),
|
|
825
|
+
...(opts?.signal ? { signal: opts.signal } : {})
|
|
770
826
|
});
|
|
771
827
|
const json = (await res.json().catch(() => null));
|
|
772
828
|
if (!res.ok || !json || json.ok === false) {
|
|
@@ -777,9 +833,13 @@ export class CompanionClient {
|
|
|
777
833
|
/** Keepalive: bump this session's last-seen time so a long-idle embed stays
|
|
778
834
|
* "live" within the session TTL (cross-app A2A friend messages keep reaching
|
|
779
835
|
* it). Cheap; call it on a timer for a background tab. Requires a live session. */
|
|
780
|
-
async ping() {
|
|
836
|
+
async ping(opts) {
|
|
781
837
|
const id = this.requireSession();
|
|
782
|
-
const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/ping`), {
|
|
838
|
+
const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/ping`), {
|
|
839
|
+
method: 'POST',
|
|
840
|
+
headers: { Authorization: `Bearer ${this.opts.token}` },
|
|
841
|
+
...(opts?.signal ? { signal: opts.signal } : {})
|
|
842
|
+
});
|
|
783
843
|
if (!res.ok) {
|
|
784
844
|
const json = (await res.json().catch(() => null));
|
|
785
845
|
throw new CompanionError(json?.error || `ping failed (${res.status})`, res.status, json?.code);
|
|
@@ -787,8 +847,8 @@ export class CompanionClient {
|
|
|
787
847
|
}
|
|
788
848
|
/** Remember a fact (into this app's namespace unless `namespace` is given and
|
|
789
849
|
* the token has the core scope). Intimate-tier writes are rejected server-side. */
|
|
790
|
-
async remember(fact) {
|
|
791
|
-
return this.postJson('/api/companion/memory', fact);
|
|
850
|
+
async remember(fact, opts) {
|
|
851
|
+
return this.postJson('/api/companion/memory', fact, opts?.signal);
|
|
792
852
|
}
|
|
793
853
|
/** Ingest a DOCUMENT into the user's knowledge base. Unlike `remember` (one
|
|
794
854
|
* short fact), this distils already-extracted text — a PDF body, a meeting
|
|
@@ -804,8 +864,8 @@ export class CompanionClient {
|
|
|
804
864
|
* are computed on the user's next first-party open (eventually-consistent
|
|
805
865
|
* semantic recall). `kind` is a free label for the source type (pdf / audio /
|
|
806
866
|
* note / …) used only for display + the materials icon. */
|
|
807
|
-
async ingestKnowledge(doc) {
|
|
808
|
-
return this.postJson('/api/companion/knowledge', doc);
|
|
867
|
+
async ingestKnowledge(doc, opts) {
|
|
868
|
+
return this.postJson('/api/companion/knowledge', doc, opts?.signal);
|
|
809
869
|
}
|
|
810
870
|
/** Convenience over `ingestKnowledge`: hand over a RAW file and Pouchy
|
|
811
871
|
* understands it server-side before ingesting — no parser/transcriber/vision on
|
|
@@ -815,8 +875,8 @@ export class CompanionClient {
|
|
|
815
875
|
* material). For other types, extract the text yourself and call
|
|
816
876
|
* `ingestKnowledge`. Same `memory.write:core` requirement and "My materials"
|
|
817
877
|
* integration as `ingestKnowledge`. */
|
|
818
|
-
async ingestFile(file) {
|
|
819
|
-
return this.postJson('/api/companion/knowledge/file', file);
|
|
878
|
+
async ingestFile(file, opts) {
|
|
879
|
+
return this.postJson('/api/companion/knowledge/file', file, opts?.signal);
|
|
820
880
|
}
|
|
821
881
|
/** Fetch the user's CURRENT companion avatar so the embedding can render the
|
|
822
882
|
* same virtual human Pouchy shows. The avatar is a VRM 3D model (`vrmUrl`,
|
|
@@ -824,9 +884,10 @@ export class CompanionClient {
|
|
|
824
884
|
* portrait when one exists (null today for built-in models). URLs are absolute
|
|
825
885
|
* and the /models asset is CORS-enabled, so a cross-origin renderer can fetch
|
|
826
886
|
* the VRM directly. Reflects the user's live choice (built-in or custom). */
|
|
827
|
-
async getAvatar() {
|
|
887
|
+
async getAvatar(opts) {
|
|
828
888
|
const res = await this.fetchAuthed(this.url('/api/companion/avatar'), {
|
|
829
|
-
headers: { Authorization: `Bearer ${this.opts.token}` }
|
|
889
|
+
headers: { Authorization: `Bearer ${this.opts.token}` },
|
|
890
|
+
...(opts?.signal ? { signal: opts.signal } : {})
|
|
830
891
|
});
|
|
831
892
|
const json = (await res.json().catch(() => null));
|
|
832
893
|
if (!res.ok || !json || json.ok === false) {
|
|
@@ -845,9 +906,10 @@ export class CompanionClient {
|
|
|
845
906
|
* pay tools, never here). Requires the `wallet.read` scope (or `wallet.spend`,
|
|
846
907
|
* which subsumes it). Same data the companion gives when asked "what's my
|
|
847
908
|
* balance?". */
|
|
848
|
-
async getWallet() {
|
|
909
|
+
async getWallet(opts) {
|
|
849
910
|
const res = await this.fetchAuthed(this.url('/api/companion/wallet'), {
|
|
850
|
-
headers: { Authorization: `Bearer ${this.opts.token}` }
|
|
911
|
+
headers: { Authorization: `Bearer ${this.opts.token}` },
|
|
912
|
+
...(opts?.signal ? { signal: opts.signal } : {})
|
|
851
913
|
});
|
|
852
914
|
const json = (await res.json().catch(() => null));
|
|
853
915
|
if (!res.ok || !json || json.ok === false) {
|
|
@@ -952,17 +1014,20 @@ export class CompanionClient {
|
|
|
952
1014
|
* MAY call `confirmAction(sameConfirmId, true)` again to re-run it (the server
|
|
953
1015
|
* dedupes a partial first attempt). A non-idempotent action (a message / skill)
|
|
954
1016
|
* stays terminal and is never retryable. */
|
|
955
|
-
async confirmAction(confirmId, approve) {
|
|
1017
|
+
async confirmAction(confirmId, approve, opts) {
|
|
956
1018
|
const sessionId = this.requireSession();
|
|
957
|
-
return this.postJson(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`, { confirmId, approve });
|
|
1019
|
+
return this.postJson(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`, { confirmId, approve }, opts?.signal);
|
|
958
1020
|
}
|
|
959
1021
|
/** The session's still-pending confirmations (display-safe: id, summary,
|
|
960
1022
|
* scope, timestamps — never raw args). Use it to rebuild your confirm card
|
|
961
1023
|
* after a reload, since confirm_request events are not replayed. Platform
|
|
962
1024
|
* session tokens only, like `confirmAction`. */
|
|
963
|
-
async pendingConfirms() {
|
|
1025
|
+
async pendingConfirms(opts) {
|
|
964
1026
|
const sessionId = this.requireSession();
|
|
965
|
-
const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`), {
|
|
1027
|
+
const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`), {
|
|
1028
|
+
headers: { Authorization: `Bearer ${this.opts.token}` },
|
|
1029
|
+
...(opts?.signal ? { signal: opts.signal } : {})
|
|
1030
|
+
});
|
|
966
1031
|
const json = (await res.json().catch(() => null));
|
|
967
1032
|
if (!res.ok || !json || json.ok === false) {
|
|
968
1033
|
throw new CompanionError(json?.error || `pending confirms failed (${res.status})`, res.status, json?.code);
|
|
@@ -1040,8 +1105,10 @@ export class CompanionClient {
|
|
|
1040
1105
|
// must not silently end reply delivery. Only a connection ERROR degrades to
|
|
1041
1106
|
// SSE, and only once. Each connection's poll timer + socket are released in
|
|
1042
1107
|
// the settle() finally so neither leaks after fallback/reconnect.
|
|
1108
|
+
let backoff = 500;
|
|
1043
1109
|
while (this.streaming && gen === this.streamGen) {
|
|
1044
1110
|
let errored = false;
|
|
1111
|
+
const connectedAt = Date.now();
|
|
1045
1112
|
try {
|
|
1046
1113
|
await new Promise((resolve, reject) => {
|
|
1047
1114
|
const ws = new WS(deriveWsStreamUrl(this.opts.baseUrl, id, this.opts.token, this.cursor));
|
|
@@ -1095,6 +1162,17 @@ export class CompanionClient {
|
|
|
1095
1162
|
return;
|
|
1096
1163
|
}
|
|
1097
1164
|
// Clean close: the while-guard reconnects if still streaming, else exits.
|
|
1165
|
+
// A SHORT-LIVED connection (gateway that accepts then immediately
|
|
1166
|
+
// closes — auth-as-close, LB idle policy, misconfigured proxy) must
|
|
1167
|
+
// back off like the SSE loop, or this reconnects in a zero-delay hot
|
|
1168
|
+
// loop. A connection that lived a while resets the backoff.
|
|
1169
|
+
if (Date.now() - connectedAt < 2000) {
|
|
1170
|
+
await this.sleep(backoff);
|
|
1171
|
+
backoff = Math.min(backoff * 2, 8000);
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
backoff = 500;
|
|
1175
|
+
}
|
|
1098
1176
|
}
|
|
1099
1177
|
}
|
|
1100
1178
|
/** Stop the event stream. */
|
|
@@ -1137,10 +1215,27 @@ export class CompanionClient {
|
|
|
1137
1215
|
return;
|
|
1138
1216
|
}
|
|
1139
1217
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1218
|
+
// Each handler is isolated: a THROWING consumer handler must not skip the
|
|
1219
|
+
// remaining handlers, and — worse — it used to propagate up through the
|
|
1220
|
+
// stream loop's catch, which read it as a CONNECTION failure (backoff +
|
|
1221
|
+
// reconnect + cursor replay); on the sendText local-emit path it rejected
|
|
1222
|
+
// a sendText whose turn had actually succeeded.
|
|
1223
|
+
for (const h of this.handlers.get(envelope.type) ?? []) {
|
|
1224
|
+
try {
|
|
1225
|
+
h(envelope);
|
|
1226
|
+
}
|
|
1227
|
+
catch (e) {
|
|
1228
|
+
console.error('[companion-sdk] event handler threw', e);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
for (const h of this.handlers.get('*') ?? []) {
|
|
1232
|
+
try {
|
|
1233
|
+
h(envelope);
|
|
1234
|
+
}
|
|
1235
|
+
catch (e) {
|
|
1236
|
+
console.error('[companion-sdk] event handler threw', e);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1144
1239
|
}
|
|
1145
1240
|
handleFrame(event, data) {
|
|
1146
1241
|
if (event === 'reconnect') {
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
+
import type { CompanionErrorCode } from './protocol.js';
|
|
2
|
+
/** Codes the SDK itself synthesizes (status 0 — never sent by the server).
|
|
3
|
+
* Runtime array so the drift test can assert every `new CompanionError(...)`
|
|
4
|
+
* call site uses a declared code; APPEND-ONLY like the server vocabulary. */
|
|
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
|
+
export type SdkSynthesizedErrorCode = (typeof SDK_SYNTHESIZED_ERROR_CODES)[number];
|
|
1
7
|
/** Thrown by every helper on an HTTP or client-side failure. `status` is the
|
|
2
8
|
* HTTP status (0 for client-synthesized failures — timeouts, unsupported
|
|
3
9
|
* environments, missing optional deps); `code` is machine-switchable when
|
|
4
10
|
* the server (or the SDK itself) names a cause. On a `rate_limited` (429)
|
|
5
11
|
* failure, `retryAfter` carries the server's Retry-After in SECONDS so a
|
|
6
12
|
* consumer can back off precisely instead of guessing. */
|
|
7
|
-
/** `CompanionError.code` values: the server vocabulary (COMPANION_ERROR_CODES
|
|
8
|
-
* plus the SDK-synthesized ones
|
|
9
|
-
*
|
|
10
|
-
|
|
13
|
+
/** `CompanionError.code` values: the server vocabulary (COMPANION_ERROR_CODES,
|
|
14
|
+
* drift-tested against api-error.ts) plus the SDK-synthesized ones — DERIVED
|
|
15
|
+
* from those runtime lists rather than hand-retyped, so a new code can never
|
|
16
|
+
* silently miss this union again (the old copy had drifted 5 codes behind).
|
|
17
|
+
* The `(string & {})` arm keeps the type open for codes newer than this SDK
|
|
18
|
+
* build while preserving autocomplete. */
|
|
19
|
+
export type CompanionErrorCodeValue = CompanionErrorCode | SdkSynthesizedErrorCode | (string & {});
|
|
11
20
|
export declare class CompanionError extends Error {
|
|
12
21
|
status: number;
|
|
13
22
|
code?: CompanionErrorCodeValue;
|
package/dist/errors.js
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
// Lives in its own module so call.ts can use it without a runtime cycle
|
|
4
4
|
// through client.ts (client.ts re-exports it, keeping the public import path
|
|
5
5
|
// `@pouchy_ai/companion-sdk` → `CompanionError` unchanged).
|
|
6
|
+
/** Codes the SDK itself synthesizes (status 0 — never sent by the server).
|
|
7
|
+
* Runtime array so the drift test can assert every `new CompanionError(...)`
|
|
8
|
+
* call site uses a declared code; APPEND-ONLY like the server vocabulary. */
|
|
9
|
+
export const SDK_SYNTHESIZED_ERROR_CODES = [
|
|
10
|
+
'reply_timeout', // awaitReply/replyTimeoutMs elapsed before the reply
|
|
11
|
+
'needs_event_stream', // turn paused on tools but start() was never called
|
|
12
|
+
'not_connected', // a call/stream helper was used before connect
|
|
13
|
+
'missing_option', // a required option was omitted (e.g. sessionId)
|
|
14
|
+
'not_representative', // pairVisitor on a non-representative token
|
|
15
|
+
'call_unsupported', // startCall on a server/provider without voice
|
|
16
|
+
'call_connect_failed', // WebRTC/provider handshake failed
|
|
17
|
+
'call_dependency_missing', // optional voice dependency not installed
|
|
18
|
+
'aborted' // a caller-supplied AbortSignal cancelled the request (0.29.0)
|
|
19
|
+
];
|
|
6
20
|
export class CompanionError extends Error {
|
|
7
21
|
status;
|
|
8
22
|
code;
|
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.29.0",
|
|
4
|
+
"description": "Embed the Pouchy companion \u2014 chat, voice, tools, memory, live world-state, instant UI, and agent-to-agent messaging \u2014 in any app, game, or site.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
7
7
|
"homepage": "https://pouchy.ai",
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
"LICENSE"
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
31
34
|
"scripts": {
|
|
32
35
|
"build": "tsc -p tsconfig.json && node scripts/fix-esm-extensions.mjs",
|
|
33
36
|
"build:cdn": "esbuild ../../src/lib/companion-sdk/index.ts --bundle --format=esm --platform=browser --external:@elevenlabs/client --legal-comments=none --outfile=cdn/companion-sdk.js && node scripts/cdn-banner.mjs",
|