@pouchy_ai/companion-sdk 0.26.0 → 0.27.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 +42 -0
- package/README.md +5 -1
- package/dist/client.d.ts +6 -1
- package/dist/client.js +40 -5
- package/dist/errors.d.ts +5 -2
- package/dist/errors.js +7 -2
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,48 @@ a protocol bump is always called out explicitly here.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.27.0] - 2026-07-12
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`rate_limited` error code + `CompanionError.retryAfter`.** The runtime's
|
|
20
|
+
two cost gates (per-user turn burst ceiling, demo daily budget) answer 429;
|
|
21
|
+
those responses now carry `code: "rate_limited"`, so the typed-error contract
|
|
22
|
+
finally covers the most common failure a busy integration hits. On any 429
|
|
23
|
+
the thrown `CompanionError` also exposes `retryAfter` (seconds, from the
|
|
24
|
+
body's `retryAfterSec` or the standard `Retry-After` header) so consumers can
|
|
25
|
+
back off precisely:
|
|
26
|
+
`if (e.code === 'rate_limited') await sleep(e.retryAfter * 1000)`.
|
|
27
|
+
- **OpenAPI now documents the 429s** (shared `RateLimited` response on every
|
|
28
|
+
model-running path) and the `input` text cap below.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **`input` text is capped at 32,000 chars** (parity with `tool-result`); an
|
|
33
|
+
over-limit turn returns 413 `payload_too_large` instead of riding a multi-MB
|
|
34
|
+
string into the model context.
|
|
35
|
+
- Malformed streaming frames no longer throw raw `SyntaxError` out of
|
|
36
|
+
`sendText`: a bad `delta` frame is skipped (the durable reply still lands),
|
|
37
|
+
and a bad `done` frame fails the turn with a clean `CompanionError` (502).
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- OpenAPI: `/api/companion/pair` no longer claims to deliver paired-friend
|
|
42
|
+
messages (pairing only; sends happen inside companion turns).
|
|
43
|
+
|
|
44
|
+
## [0.26.1] - 2026-07-12
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **Docs: `/api/stt` now requires auth.** The audio-transcription relay the
|
|
49
|
+
`ingestKnowledge` guidance points at is no longer an open endpoint — send
|
|
50
|
+
your `pchy_…` access token as the bearer (any valid token works; the call
|
|
51
|
+
is metered like other companion ops). No SDK code paths changed; this
|
|
52
|
+
release updates the inline JSDoc guidance only.
|
|
53
|
+
- Server-side hardening (no client change needed): concurrent `input` posts
|
|
54
|
+
on one session now lose cleanly with the existing 409 `turn_pending` code
|
|
55
|
+
instead of double-running a turn — retry after the in-flight reply lands.
|
|
56
|
+
|
|
15
57
|
## [0.26.0] - 2026-07-11
|
|
16
58
|
|
|
17
59
|
### Fixed
|
package/README.md
CHANGED
|
@@ -147,7 +147,11 @@ 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).
|
|
150
|
+
(0.24.0 — previously only the POST-backed calls did). 0.27.0 adds
|
|
151
|
+
`rate_limited` for the runtime's 429 cost gates (turn burst ceiling, demo
|
|
152
|
+
daily budget); on those failures `CompanionError.retryAfter` carries the
|
|
153
|
+
server's Retry-After in seconds, so back off with
|
|
154
|
+
`if (e.code === 'rate_limited') await sleep(e.retryAfter * 1000)`. The SDK also synthesizes
|
|
151
155
|
a few client-side codes outside that HTTP vocabulary: `reply_timeout`
|
|
152
156
|
(awaitReply fallback gave up), `stream_unauthorized` (event-stream 401
|
|
153
157
|
exhausted reconnects), and — 0.26.0 — the voice connect-step codes
|
package/dist/client.d.ts
CHANGED
|
@@ -228,6 +228,10 @@ export declare class CompanionClient {
|
|
|
228
228
|
private url;
|
|
229
229
|
private jsonHeaders;
|
|
230
230
|
private requireSession;
|
|
231
|
+
/** Retry-After in seconds for a throttled response: the JSON body's
|
|
232
|
+
* `retryAfterSec` (companion rate-limit shape) wins, else the standard
|
|
233
|
+
* Retry-After header. Undefined when the server named neither. */
|
|
234
|
+
private retryAfterFrom;
|
|
231
235
|
private postJson;
|
|
232
236
|
/** Handshake: start or resume the session for this surface. */
|
|
233
237
|
connect(): Promise<HelloAck>;
|
|
@@ -432,7 +436,8 @@ export declare class CompanionClient {
|
|
|
432
436
|
* materials" list with source attribution, exactly like a first-party upload.
|
|
433
437
|
* So the materials entry point can live in YOUR app, not just Pouchy's.
|
|
434
438
|
*
|
|
435
|
-
* Extract the text yourself (your own parser, or Pouchy's /api/stt for audio
|
|
439
|
+
* Extract the text yourself (your own parser, or Pouchy's /api/stt for audio —
|
|
440
|
+
* send your pchy_ token as the bearer; the relay is auth-gated and metered)
|
|
436
441
|
* and pass it here. Writes the user's SHARED knowledge, so the token must
|
|
437
442
|
* hold `memory.write:core` (the user's consent) — otherwise 403. Embeddings
|
|
438
443
|
* are computed on the user's next first-party open (eventually-consistent
|
package/dist/client.js
CHANGED
|
@@ -119,6 +119,16 @@ export class CompanionClient {
|
|
|
119
119
|
throw new CompanionError('CompanionClient: call connect() first', 0, 'not_connected');
|
|
120
120
|
return this._session;
|
|
121
121
|
}
|
|
122
|
+
/** Retry-After in seconds for a throttled response: the JSON body's
|
|
123
|
+
* `retryAfterSec` (companion rate-limit shape) wins, else the standard
|
|
124
|
+
* Retry-After header. Undefined when the server named neither. */
|
|
125
|
+
retryAfterFrom(res, json) {
|
|
126
|
+
const fromBody = json?.retryAfterSec;
|
|
127
|
+
if (typeof fromBody === 'number' && Number.isFinite(fromBody) && fromBody >= 0)
|
|
128
|
+
return fromBody;
|
|
129
|
+
const header = Number(res.headers.get('Retry-After'));
|
|
130
|
+
return Number.isFinite(header) && header >= 0 ? header : undefined;
|
|
131
|
+
}
|
|
122
132
|
async postJson(path, body) {
|
|
123
133
|
const res = await this.fetchAuthed(this.url(path), {
|
|
124
134
|
method: 'POST',
|
|
@@ -127,7 +137,7 @@ export class CompanionClient {
|
|
|
127
137
|
});
|
|
128
138
|
const json = (await res.json().catch(() => null));
|
|
129
139
|
if (!res.ok || !json || json.ok === false) {
|
|
130
|
-
throw new CompanionError(json?.error || `request failed (${res.status})`, res.status, json?.code);
|
|
140
|
+
throw new CompanionError(json?.error || `request failed (${res.status})`, res.status, json?.code, this.retryAfterFrom(res, json));
|
|
131
141
|
}
|
|
132
142
|
return json;
|
|
133
143
|
}
|
|
@@ -238,7 +248,7 @@ export class CompanionClient {
|
|
|
238
248
|
// Old server / error before the stream opened — behave like postJson.
|
|
239
249
|
const json = (await res.json().catch(() => null));
|
|
240
250
|
if (!res.ok || !json || json.ok === false) {
|
|
241
|
-
throw new CompanionError(json?.error || `request failed (${res.status})`, res.status, json?.code);
|
|
251
|
+
throw new CompanionError(json?.error || `request failed (${res.status})`, res.status, json?.code, this.retryAfterFrom(res, json));
|
|
242
252
|
}
|
|
243
253
|
return { seq: json.seq ?? null };
|
|
244
254
|
}
|
|
@@ -259,7 +269,16 @@ export class CompanionClient {
|
|
|
259
269
|
buf = rest;
|
|
260
270
|
for (const ev of events) {
|
|
261
271
|
if (ev.event === 'delta') {
|
|
262
|
-
|
|
272
|
+
// A malformed delta frame is dropped, not fatal — losing one text
|
|
273
|
+
// chunk beats throwing away the whole turn (the final message
|
|
274
|
+
// arrives intact in the done frame / event stream regardless).
|
|
275
|
+
let d;
|
|
276
|
+
try {
|
|
277
|
+
d = JSON.parse(ev.data);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
263
282
|
if (typeof d.text === 'string' && d.text)
|
|
264
283
|
fireDelta(d.text, {});
|
|
265
284
|
}
|
|
@@ -267,7 +286,22 @@ export class CompanionClient {
|
|
|
267
286
|
fireDelta('', { reset: true });
|
|
268
287
|
}
|
|
269
288
|
else if (ev.event === 'done') {
|
|
270
|
-
|
|
289
|
+
// The done frame carries the turn's resolution — unparseable means
|
|
290
|
+
// the turn's outcome is unknowable, so fail it explicitly instead
|
|
291
|
+
// of letting a raw SyntaxError escape the SDK.
|
|
292
|
+
let d;
|
|
293
|
+
try {
|
|
294
|
+
d = JSON.parse(ev.data);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
try {
|
|
298
|
+
await reader.cancel();
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
/* stream already finished */
|
|
302
|
+
}
|
|
303
|
+
throw new CompanionError('malformed done frame', 502);
|
|
304
|
+
}
|
|
271
305
|
try {
|
|
272
306
|
await reader.cancel();
|
|
273
307
|
}
|
|
@@ -642,7 +676,8 @@ export class CompanionClient {
|
|
|
642
676
|
* materials" list with source attribution, exactly like a first-party upload.
|
|
643
677
|
* So the materials entry point can live in YOUR app, not just Pouchy's.
|
|
644
678
|
*
|
|
645
|
-
* Extract the text yourself (your own parser, or Pouchy's /api/stt for audio
|
|
679
|
+
* Extract the text yourself (your own parser, or Pouchy's /api/stt for audio —
|
|
680
|
+
* send your pchy_ token as the bearer; the relay is auth-gated and metered)
|
|
646
681
|
* and pass it here. Writes the user's SHARED knowledge, so the token must
|
|
647
682
|
* hold `memory.write:core` (the user's consent) — otherwise 403. Embeddings
|
|
648
683
|
* are computed on the user's next first-party open (eventually-consistent
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/** Thrown by every helper on an HTTP or client-side failure. `status` is the
|
|
2
2
|
* HTTP status (0 for client-synthesized failures — timeouts, unsupported
|
|
3
3
|
* environments, missing optional deps); `code` is machine-switchable when
|
|
4
|
-
* the server (or the SDK itself) names a cause.
|
|
4
|
+
* the server (or the SDK itself) names a cause. On a `rate_limited` (429)
|
|
5
|
+
* failure, `retryAfter` carries the server's Retry-After in SECONDS so a
|
|
6
|
+
* consumer can back off precisely instead of guessing. */
|
|
5
7
|
export declare class CompanionError extends Error {
|
|
6
8
|
status: number;
|
|
7
9
|
code?: string;
|
|
8
|
-
|
|
10
|
+
retryAfter?: number;
|
|
11
|
+
constructor(message: string, status: number, code?: string, retryAfter?: number);
|
|
9
12
|
}
|
package/dist/errors.js
CHANGED
|
@@ -6,15 +6,20 @@
|
|
|
6
6
|
/** Thrown by every helper on an HTTP or client-side failure. `status` is the
|
|
7
7
|
* HTTP status (0 for client-synthesized failures — timeouts, unsupported
|
|
8
8
|
* environments, missing optional deps); `code` is machine-switchable when
|
|
9
|
-
* the server (or the SDK itself) names a cause.
|
|
9
|
+
* the server (or the SDK itself) names a cause. On a `rate_limited` (429)
|
|
10
|
+
* failure, `retryAfter` carries the server's Retry-After in SECONDS so a
|
|
11
|
+
* consumer can back off precisely instead of guessing. */
|
|
10
12
|
export class CompanionError extends Error {
|
|
11
13
|
status;
|
|
12
14
|
code;
|
|
13
|
-
|
|
15
|
+
retryAfter;
|
|
16
|
+
constructor(message, status, code, retryAfter) {
|
|
14
17
|
super(message);
|
|
15
18
|
this.name = 'CompanionError';
|
|
16
19
|
this.status = status;
|
|
17
20
|
if (code)
|
|
18
21
|
this.code = code;
|
|
22
|
+
if (typeof retryAfter === 'number' && Number.isFinite(retryAfter))
|
|
23
|
+
this.retryAfter = retryAfter;
|
|
19
24
|
}
|
|
20
25
|
}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type MessageType = InboundType | OutboundType;
|
|
|
16
16
|
* self-contained mirror of the server's api-error.ts list (drift-tested);
|
|
17
17
|
* APPEND-ONLY: renaming or removing a code is a breaking SDK change.
|
|
18
18
|
* Consumers can switch on these instead of string-matching error prose. */
|
|
19
|
-
export declare const COMPANION_ERROR_CODES: readonly ["missing_token", "invalid_token", "missing_scope", "invalid_request", "session_not_found", "turn_pending", "no_pending_tools", "unknown_call", "payload_too_large", "forbidden", "unavailable"];
|
|
19
|
+
export declare const COMPANION_ERROR_CODES: readonly ["missing_token", "invalid_token", "missing_scope", "invalid_request", "session_not_found", "turn_pending", "no_pending_tools", "unknown_call", "payload_too_large", "rate_limited", "forbidden", "unavailable"];
|
|
20
20
|
export type CompanionErrorCode = (typeof COMPANION_ERROR_CODES)[number];
|
|
21
21
|
/** The single envelope every control/data-plane message shares. */
|
|
22
22
|
export interface CompanionEnvelope<T = unknown> {
|
package/dist/protocol.js
CHANGED
|
@@ -54,6 +54,7 @@ export const COMPANION_ERROR_CODES = [
|
|
|
54
54
|
'no_pending_tools', // 409 — tool result posted but nothing is pending
|
|
55
55
|
'unknown_call', // 404 — tool result for a callId the turn didn't issue
|
|
56
56
|
'payload_too_large', // 413 — body/image over the documented cap
|
|
57
|
+
'rate_limited', // 429 — turn burst / demo daily budget spent; Retry-After + retryAfterSec say when
|
|
57
58
|
'forbidden', // 403 — authorization denial other than a missing scope
|
|
58
59
|
'unavailable' // 503 — backing store/provider not configured or down
|
|
59
60
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pouchy_ai/companion-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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",
|