@pouchy_ai/companion-sdk 0.26.1 → 0.28.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 +85 -0
- package/README.md +13 -4
- package/dist/call.js +11 -1
- package/dist/client.d.ts +16 -1
- package/dist/client.js +226 -69
- package/dist/errors.d.ts +10 -3
- package/dist/errors.js +4 -5
- package/dist/index.d.ts +2 -1
- package/dist/protocol.d.ts +9 -1
- package/dist/protocol.js +6 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,91 @@ a protocol bump is always called out explicitly here.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.28.0] - 2026-07-12
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Turn correlation (`turnId` → `replyTo`).** Every `sendText` now mints a
|
|
20
|
+
`turnId` and sends it with the turn; servers on API ≥ 1.1 echo it back as
|
|
21
|
+
`replyTo` on the reply's `companion.message` — including a reply that lands
|
|
22
|
+
after an app-tool pause. `awaitReply`'s buffered fallback now resolves ONLY
|
|
23
|
+
on the correlated reply on those servers, so a proactive world-state
|
|
24
|
+
reaction or a confirm outcome can no longer steal the resolution (the known
|
|
25
|
+
0.25–0.27 caveat). Against older servers the legacy resolve-on-first
|
|
26
|
+
behavior is kept automatically (capability-detected via
|
|
27
|
+
`X-Pouchy-Api-Version`). New `MessagePayload` type (`{ text, replyTo? }`).
|
|
28
|
+
- **`needs_event_stream` error.** With `awaitReply` and no `start()`, a turn
|
|
29
|
+
that pauses on app tool calls used to wait out the full `replyTimeoutMs`
|
|
30
|
+
and then time out — the post-resume reply can only arrive on the event
|
|
31
|
+
stream. It now rejects immediately with `code: "needs_event_stream"`.
|
|
32
|
+
- **Typed confirm errors.** The confirm/step-up endpoints now carry codes:
|
|
33
|
+
`confirm_not_found`, `confirm_resolved`, `step_up_required`,
|
|
34
|
+
`step_up_failed` (vocabulary is append-only). The client no longer burns an
|
|
35
|
+
`onAuthError` token refresh on a step-up 401.
|
|
36
|
+
- **`sendWorldState` returns `injected` and `reacted`** — whether the batch
|
|
37
|
+
triggered a live-call inject or a proactive text reaction (previously
|
|
38
|
+
discarded).
|
|
39
|
+
- **429 `hint` surfaced.** The demo-budget hint ("provision a project…") is
|
|
40
|
+
appended to the thrown `CompanionError` message instead of being dropped.
|
|
41
|
+
- **`CompanionErrorCodeValue`** — `CompanionError.code` is now typed as the
|
|
42
|
+
known vocabulary union (plus open `string`), so `switch` statements get
|
|
43
|
+
autocomplete and exhaustiveness help.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **`replyTimeoutMs` now bounds the WHOLE awaitReply operation.** Previously
|
|
48
|
+
only the buffered-fallback wait was bounded — a stalled streaming response
|
|
49
|
+
could hang the promise indefinitely. The deadline now starts before the
|
|
50
|
+
POST and aborts the in-flight stream on expiry.
|
|
51
|
+
- **`stop()` immediately followed by `start()` no longer leaves two live
|
|
52
|
+
receive loops.** Loops carry a generation token; a stale loop exits instead
|
|
53
|
+
of resurrecting alongside the new one (it double-polled the stream forever).
|
|
54
|
+
- **`connectCall` cleans up on provider-initiated teardown.** A dropped call
|
|
55
|
+
(pc failure / provider disconnect) now runs the same unsubscribe +
|
|
56
|
+
`endSession({ transcript })` consolidation that `close()` runs — previously
|
|
57
|
+
the voice_inject listener leaked and the call was never consolidated into
|
|
58
|
+
memory. The buffered transcript is also capped (drop-oldest 200).
|
|
59
|
+
- **A throwing `onDelta` handler no longer leaks the streaming reader** — the
|
|
60
|
+
stream is cancelled before the error propagates.
|
|
61
|
+
- **`remember`'s `category` is now typed `'relationship' |
|
|
62
|
+
'shared_experience'`** — the only two values the server keeps; anything
|
|
63
|
+
else was silently discarded.
|
|
64
|
+
- **`/input` image constraints now error instead of silently dropping.** A
|
|
65
|
+
non-`data:` URL or a 5th image is a 400 `invalid_request` — previously the
|
|
66
|
+
image was filtered out and the model just never saw it.
|
|
67
|
+
- **OpenAPI drift**: `/input` documents `stream` + `turnId` + image rules;
|
|
68
|
+
confirm 200 documents `exec_failed`/`outcome`/`retryable`; hello.ack lists
|
|
69
|
+
`modalities`/`representative`/`visitorPaired`; `/pair` documents its body.
|
|
70
|
+
|
|
71
|
+
## [0.27.0] - 2026-07-12
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- **`rate_limited` error code + `CompanionError.retryAfter`.** The runtime's
|
|
76
|
+
two cost gates (per-user turn burst ceiling, demo daily budget) answer 429;
|
|
77
|
+
those responses now carry `code: "rate_limited"`, so the typed-error contract
|
|
78
|
+
finally covers the most common failure a busy integration hits. On any 429
|
|
79
|
+
the thrown `CompanionError` also exposes `retryAfter` (seconds, from the
|
|
80
|
+
body's `retryAfterSec` or the standard `Retry-After` header) so consumers can
|
|
81
|
+
back off precisely:
|
|
82
|
+
`if (e.code === 'rate_limited') await sleep(e.retryAfter * 1000)`.
|
|
83
|
+
- **OpenAPI now documents the 429s** (shared `RateLimited` response on every
|
|
84
|
+
model-running path) and the `input` text cap below.
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
|
|
88
|
+
- **`input` text is capped at 32,000 chars** (parity with `tool-result`); an
|
|
89
|
+
over-limit turn returns 413 `payload_too_large` instead of riding a multi-MB
|
|
90
|
+
string into the model context.
|
|
91
|
+
- Malformed streaming frames no longer throw raw `SyntaxError` out of
|
|
92
|
+
`sendText`: a bad `delta` frame is skipped (the durable reply still lands),
|
|
93
|
+
and a bad `done` frame fails the turn with a clean `CompanionError` (502).
|
|
94
|
+
|
|
95
|
+
### Fixed
|
|
96
|
+
|
|
97
|
+
- OpenAPI: `/api/companion/pair` no longer claims to deliver paired-friend
|
|
98
|
+
messages (pairing only; sends happen inside companion turns).
|
|
99
|
+
|
|
15
100
|
## [0.26.1] - 2026-07-12
|
|
16
101
|
|
|
17
102
|
### Changed
|
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ local/device skills).
|
|
|
96
96
|
|
|
97
97
|
| Method | What it does |
|
|
98
98
|
|---|---|
|
|
99
|
-
| `sendText(text, { awaitReply: true, replyTimeoutMs? })` | Request/response mode: the promise resolves with the completed turn's reply `{ seq, text, envelope }` (`SendTextReply`) — no `onMessage` wiring or hand-rolled turn gate. Works without `start()` (the reply rides the same streaming request)
|
|
99
|
+
| `sendText(text, { awaitReply: true, replyTimeoutMs? })` | Request/response mode: the promise resolves with the completed turn's reply `{ seq, text, envelope }` (`SendTextReply`) — no `onMessage` wiring or hand-rolled turn gate. Works without `start()` when the turn completes in text (the reply rides the same streaming request). A turn that pauses on YOUR declared tools resolves via the event stream, so it DOES need `start()` — without it the promise rejects immediately with `code: 'needs_event_stream'`. `replyTimeoutMs` (default 60s) bounds the whole operation (0.28.0); on expiry it aborts the in-flight stream and rejects with `code: 'reply_timeout'`. Each turn carries a minted `turnId` the server echoes as `replyTo` on the reply (API ≥ 1.1), so the fallback resolves on THE reply — proactive messages can't steal it. |
|
|
100
100
|
| `recall({ query?, limit? })` / `remember({ content, … })` | Read / write the token-scoped memory namespace. `query` runs SEMANTIC search over the facts server-side (embedding-based, ranked fallback); omit it for the plain importance/recency ranking. |
|
|
101
101
|
| `ingestKnowledge({ text, name, … })` / `ingestFile(file)` | Push documents into the user's knowledge base ("My materials"); needs `memory.write:core`. |
|
|
102
102
|
| `history({ limit? })` | Fetch the session transcript — restore chat on reload. |
|
|
@@ -147,15 +147,24 @@ 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
|
|
154
158
|
`call_unsupported` (no WebRTC/mic in this environment), `call_connect_failed`
|
|
155
159
|
(mic timeout / SDP exchange failed) and `call_dependency_missing`
|
|
156
|
-
(`@elevenlabs/client` not installed).
|
|
160
|
+
(`@elevenlabs/client` not installed). 0.28.0 adds the confirm/step-up codes
|
|
161
|
+
(`confirm_not_found`, `confirm_resolved`, `step_up_required`, `step_up_failed`),
|
|
162
|
+
the client-side `needs_event_stream`, and types `CompanionError.code` as the
|
|
163
|
+
`CompanionErrorCodeValue` union (autocomplete in your `switch`; still open for
|
|
164
|
+
newer servers). Every
|
|
157
165
|
outbound payload has a named type; `ToolCallPayload` (`{ id, name, args }`) types
|
|
158
|
-
the `companion.tool_call` event,
|
|
166
|
+
the `companion.tool_call` event, `MessagePayload` (`{ text, replyTo? }`) types
|
|
167
|
+
`companion.message`, and `onToolCall`'s callback receives
|
|
159
168
|
`ToolCallEvent` — the payload plus `argsJson` (pre-parsed args).
|
|
160
169
|
|
|
161
170
|
For advanced voice hosts, the lower-level `openCompanionCall(client, options)`
|
package/dist/call.js
CHANGED
|
@@ -234,7 +234,17 @@ async function openOpenAICall(creds, opts, bridge) {
|
|
|
234
234
|
const dc = pc.createDataChannel('oai-events');
|
|
235
235
|
// `response.function_call_arguments.done` carries the call_id + args but often
|
|
236
236
|
// not the name — the name arrives earlier on `response.output_item.added`; track it.
|
|
237
|
+
// Cancelled tool calls never reach `arguments.done`, so their entries were
|
|
238
|
+
// never deleted — cap the map (drop-oldest) so a long call can't grow it.
|
|
237
239
|
const callNames = new Map();
|
|
240
|
+
const rememberCallName = (id, name) => {
|
|
241
|
+
callNames.set(id, name);
|
|
242
|
+
if (callNames.size > 64) {
|
|
243
|
+
const first = callNames.keys().next().value;
|
|
244
|
+
if (first !== undefined)
|
|
245
|
+
callNames.delete(first);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
238
248
|
if (bridge?.tools.length) {
|
|
239
249
|
dc.addEventListener('open', () => {
|
|
240
250
|
try {
|
|
@@ -271,7 +281,7 @@ async function openOpenAICall(creds, opts, bridge) {
|
|
|
271
281
|
msg.item?.type === 'function_call' &&
|
|
272
282
|
msg.item.call_id &&
|
|
273
283
|
msg.item.name) {
|
|
274
|
-
|
|
284
|
+
rememberCallName(msg.item.call_id, msg.item.name);
|
|
275
285
|
}
|
|
276
286
|
if (bridge && msg.type === 'response.function_call_arguments.done' && msg.call_id) {
|
|
277
287
|
const callId = msg.call_id;
|
package/dist/client.d.ts
CHANGED
|
@@ -205,6 +205,7 @@ export declare class CompanionClient {
|
|
|
205
205
|
private cursor;
|
|
206
206
|
private streaming;
|
|
207
207
|
private abort;
|
|
208
|
+
private streamGen;
|
|
208
209
|
private readonly handlers;
|
|
209
210
|
private readonly seen;
|
|
210
211
|
private readonly deltaHandlers;
|
|
@@ -224,10 +225,20 @@ export declare class CompanionClient {
|
|
|
224
225
|
/** doFetch with one transparent retry after a 401-triggered token refresh.
|
|
225
226
|
* Everything except the stream loop (which has its own auth handling) goes
|
|
226
227
|
* through here. */
|
|
228
|
+
private serverApiVersion;
|
|
229
|
+
/** True when the server echoes `replyTo` on companion.message (API ≥ 1.1) —
|
|
230
|
+
* the strict awaitReply correlation is only safe to enforce there. */
|
|
231
|
+
private serverEchoesReplyTo;
|
|
227
232
|
private fetchAuthed;
|
|
233
|
+
/** Peek the error `code` of a failure response without consuming its body. */
|
|
234
|
+
private errorCodeOf;
|
|
228
235
|
private url;
|
|
229
236
|
private jsonHeaders;
|
|
230
237
|
private requireSession;
|
|
238
|
+
/** Retry-After in seconds for a throttled response: the JSON body's
|
|
239
|
+
* `retryAfterSec` (companion rate-limit shape) wins, else the standard
|
|
240
|
+
* Retry-After header. Undefined when the server named neither. */
|
|
241
|
+
private retryAfterFrom;
|
|
231
242
|
private postJson;
|
|
232
243
|
/** Handshake: start or resume the session for this surface. */
|
|
233
244
|
connect(): Promise<HelloAck>;
|
|
@@ -304,6 +315,8 @@ export declare class CompanionClient {
|
|
|
304
315
|
sendWorldState(event: WorldStateInput | WorldStateInput[]): Promise<{
|
|
305
316
|
accepted: number;
|
|
306
317
|
dropped: number;
|
|
318
|
+
injected: number;
|
|
319
|
+
reacted: boolean;
|
|
307
320
|
}>;
|
|
308
321
|
private randomId;
|
|
309
322
|
/** Open the voice plane. Returns realtime credentials to connect DIRECTLY to
|
|
@@ -417,7 +430,9 @@ export declare class CompanionClient {
|
|
|
417
430
|
content: string;
|
|
418
431
|
importance?: number;
|
|
419
432
|
confidence?: number;
|
|
420
|
-
|
|
433
|
+
/** Only these two are kept server-side — anything else is discarded, so
|
|
434
|
+
* the type refuses it at compile time instead of dropping it silently. */
|
|
435
|
+
category?: 'relationship' | 'shared_experience';
|
|
421
436
|
kind?: string;
|
|
422
437
|
namespace?: string;
|
|
423
438
|
sensitivity?: 'public' | 'personal';
|
package/dist/client.js
CHANGED
|
@@ -50,6 +50,11 @@ export class CompanionClient {
|
|
|
50
50
|
cursor = 0;
|
|
51
51
|
streaming = false;
|
|
52
52
|
abort = null;
|
|
53
|
+
// Bumped on every start()/stop(): a loop whose generation is stale exits
|
|
54
|
+
// instead of resurrecting — stop() immediately followed by start() used to
|
|
55
|
+
// leave TWO concurrent receive loops polling forever (the old one saw
|
|
56
|
+
// `streaming === true` again after its backoff sleep).
|
|
57
|
+
streamGen = 0;
|
|
53
58
|
handlers = new Map();
|
|
54
59
|
seen = new Set();
|
|
55
60
|
// Token-streaming delta subscribers (P1-2). When any are registered,
|
|
@@ -101,13 +106,47 @@ export class CompanionClient {
|
|
|
101
106
|
/** doFetch with one transparent retry after a 401-triggered token refresh.
|
|
102
107
|
* Everything except the stream loop (which has its own auth handling) goes
|
|
103
108
|
* through here. */
|
|
109
|
+
// The running server's X-Pouchy-Api-Version (captured off every authed
|
|
110
|
+
// response) — capability detection for additive protocol features.
|
|
111
|
+
serverApiVersion = null;
|
|
112
|
+
/** True when the server echoes `replyTo` on companion.message (API ≥ 1.1) —
|
|
113
|
+
* the strict awaitReply correlation is only safe to enforce there. */
|
|
114
|
+
serverEchoesReplyTo() {
|
|
115
|
+
const m = /^(\d+)\.(\d+)/.exec(this.serverApiVersion ?? '');
|
|
116
|
+
if (!m)
|
|
117
|
+
return false;
|
|
118
|
+
const major = Number(m[1]);
|
|
119
|
+
const minor = Number(m[2]);
|
|
120
|
+
return major > 1 || (major === 1 && minor >= 1);
|
|
121
|
+
}
|
|
104
122
|
async fetchAuthed(url, init) {
|
|
105
123
|
const res = await this.doFetch(url, init);
|
|
106
|
-
|
|
124
|
+
const apiVersion = res.headers.get('x-pouchy-api-version');
|
|
125
|
+
if (apiVersion)
|
|
126
|
+
this.serverApiVersion = apiVersion;
|
|
127
|
+
if (res.status !== 401)
|
|
128
|
+
return res;
|
|
129
|
+
// A step-up 401 (biometric confirm) is NOT a token problem — refreshing
|
|
130
|
+
// and retrying would burn a backend re-mint on a doomed request. Only
|
|
131
|
+
// token-shaped 401s go through onAuthError.
|
|
132
|
+
const code = await this.errorCodeOf(res);
|
|
133
|
+
if (code === 'step_up_required' || code === 'step_up_failed')
|
|
134
|
+
return res;
|
|
135
|
+
if (!(await this.tryRefreshToken()))
|
|
107
136
|
return res;
|
|
108
137
|
init.headers.Authorization = `Bearer ${this.opts.token}`;
|
|
109
138
|
return this.doFetch(url, init);
|
|
110
139
|
}
|
|
140
|
+
/** Peek the error `code` of a failure response without consuming its body. */
|
|
141
|
+
async errorCodeOf(res) {
|
|
142
|
+
try {
|
|
143
|
+
const json = (await res.clone().json());
|
|
144
|
+
return typeof json?.code === 'string' ? json.code : undefined;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
111
150
|
url(path) {
|
|
112
151
|
return this.opts.baseUrl.replace(/\/+$/, '') + path;
|
|
113
152
|
}
|
|
@@ -119,6 +158,16 @@ export class CompanionClient {
|
|
|
119
158
|
throw new CompanionError('CompanionClient: call connect() first', 0, 'not_connected');
|
|
120
159
|
return this._session;
|
|
121
160
|
}
|
|
161
|
+
/** Retry-After in seconds for a throttled response: the JSON body's
|
|
162
|
+
* `retryAfterSec` (companion rate-limit shape) wins, else the standard
|
|
163
|
+
* Retry-After header. Undefined when the server named neither. */
|
|
164
|
+
retryAfterFrom(res, json) {
|
|
165
|
+
const fromBody = json?.retryAfterSec;
|
|
166
|
+
if (typeof fromBody === 'number' && Number.isFinite(fromBody) && fromBody >= 0)
|
|
167
|
+
return fromBody;
|
|
168
|
+
const header = Number(res.headers.get('Retry-After'));
|
|
169
|
+
return Number.isFinite(header) && header >= 0 ? header : undefined;
|
|
170
|
+
}
|
|
122
171
|
async postJson(path, body) {
|
|
123
172
|
const res = await this.fetchAuthed(this.url(path), {
|
|
124
173
|
method: 'POST',
|
|
@@ -127,7 +176,11 @@ export class CompanionClient {
|
|
|
127
176
|
});
|
|
128
177
|
const json = (await res.json().catch(() => null));
|
|
129
178
|
if (!res.ok || !json || json.ok === false) {
|
|
130
|
-
|
|
179
|
+
// The 429 body carries an actionable `hint` (e.g. "provision a project
|
|
180
|
+
// …") — surface it instead of discarding it.
|
|
181
|
+
const hint = json?.hint;
|
|
182
|
+
const detail = typeof hint === 'string' && hint ? ` — ${hint}` : '';
|
|
183
|
+
throw new CompanionError((json?.error || `request failed (${res.status})`) + detail, res.status, json?.code, this.retryAfterFrom(res, json));
|
|
131
184
|
}
|
|
132
185
|
return json;
|
|
133
186
|
}
|
|
@@ -172,9 +225,14 @@ export class CompanionClient {
|
|
|
172
225
|
}
|
|
173
226
|
async sendText(text, opts) {
|
|
174
227
|
const id = this.requireSession();
|
|
175
|
-
|
|
228
|
+
// Client-minted correlation id (0.28.0): the server echoes it back as
|
|
229
|
+
// `replyTo` on this turn's companion.message — even across an app-tool
|
|
230
|
+
// pause — so awaitReply's fallback matches THE reply, not the first
|
|
231
|
+
// message that happens by (world-state reactions, confirm outcomes).
|
|
232
|
+
const turnId = `turn_${this.randomId()}`;
|
|
233
|
+
const body = { text, turnId, ...(opts?.images?.length ? { images: opts.images } : {}) };
|
|
176
234
|
if (opts?.awaitReply)
|
|
177
|
-
return this.sendTextAwaitingReply(id, body, opts);
|
|
235
|
+
return this.sendTextAwaitingReply(id, body, opts, turnId);
|
|
178
236
|
const wantStream = opts?.stream ?? this.deltaHandlers.size > 0;
|
|
179
237
|
if (!wantStream) {
|
|
180
238
|
const json = await this.postJson(`/api/companion/session/${id}/input`, body);
|
|
@@ -187,31 +245,61 @@ export class CompanionClient {
|
|
|
187
245
|
* `companion.message` off the event stream, bounded by a timeout. The
|
|
188
246
|
* fallback subscription opens BEFORE the POST so a fast event-stream reply
|
|
189
247
|
* can't slip through the gap. */
|
|
190
|
-
async sendTextAwaitingReply(sessionId, body, opts) {
|
|
248
|
+
async sendTextAwaitingReply(sessionId, body, opts, turnId) {
|
|
191
249
|
const timeoutMs = opts?.replyTimeoutMs ?? 60_000;
|
|
192
250
|
let resolveFallback;
|
|
193
251
|
const fallback = new Promise((res) => (resolveFallback = res));
|
|
194
|
-
|
|
252
|
+
// Correlate: resolve only on THIS turn's reply (`replyTo === turnId`).
|
|
253
|
+
// Servers on API ≥ 1.1 always echo it, so there an un-tagged message is
|
|
254
|
+
// PROACTIVE traffic (world-state reaction, confirm outcome) — skip it.
|
|
255
|
+
// Older servers never echo — keep the legacy resolve-on-first there.
|
|
256
|
+
const off = this.on('companion.message', (env) => {
|
|
257
|
+
const replyTo = env.payload?.replyTo;
|
|
258
|
+
if (replyTo === turnId)
|
|
259
|
+
return resolveFallback(env);
|
|
260
|
+
if (replyTo === undefined && !this.serverEchoesReplyTo())
|
|
261
|
+
resolveFallback(env);
|
|
262
|
+
});
|
|
263
|
+
// One deadline bounds the WHOLE operation (0.28.0): the timer starts
|
|
264
|
+
// before the POST, so a stalled streaming response can no longer hang the
|
|
265
|
+
// promise past replyTimeoutMs (previously only the fallback wait was
|
|
266
|
+
// bounded). The abort tears the in-flight stream down with it.
|
|
267
|
+
const ac = new AbortController();
|
|
195
268
|
let timer;
|
|
269
|
+
const deadline = new Promise((_, reject) => {
|
|
270
|
+
timer = setTimeout(() => {
|
|
271
|
+
ac.abort();
|
|
272
|
+
reject(new CompanionError(`timed out after ${timeoutMs}ms waiting for the reply — buffered mode needs start() polling the event stream`, 0, 'reply_timeout'));
|
|
273
|
+
}, timeoutMs);
|
|
274
|
+
});
|
|
196
275
|
try {
|
|
197
276
|
let seq = null;
|
|
198
277
|
let envelope;
|
|
278
|
+
let pausedOnTools = false;
|
|
199
279
|
if (opts?.stream === false) {
|
|
200
|
-
const json = await
|
|
280
|
+
const json = await Promise.race([
|
|
281
|
+
this.postJson(`/api/companion/session/${sessionId}/input`, body),
|
|
282
|
+
deadline
|
|
283
|
+
]);
|
|
201
284
|
seq = json.seq ?? null;
|
|
202
285
|
}
|
|
203
286
|
else {
|
|
204
|
-
const r = await
|
|
287
|
+
const r = await Promise.race([
|
|
288
|
+
this.sendTextStreaming(sessionId, body, ac.signal),
|
|
289
|
+
deadline
|
|
290
|
+
]);
|
|
205
291
|
seq = r.seq;
|
|
206
292
|
envelope = r.envelope;
|
|
293
|
+
pausedOnTools = r.kind === 'tool_calls';
|
|
207
294
|
}
|
|
208
295
|
if (!envelope) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
296
|
+
// A turn paused on app tool calls resolves via the event stream once
|
|
297
|
+
// the app posts results — without start() those events never arrive,
|
|
298
|
+
// so waiting out the timer is a guaranteed hang. Fail fast + say why.
|
|
299
|
+
if (pausedOnTools && !this.streaming) {
|
|
300
|
+
throw new CompanionError('the turn paused on app tool calls — awaitReply needs start() (the event stream) to receive the post-resume reply', 0, 'needs_event_stream');
|
|
301
|
+
}
|
|
302
|
+
envelope = await Promise.race([fallback, deadline]);
|
|
215
303
|
}
|
|
216
304
|
const replyText = envelope.payload?.text;
|
|
217
305
|
return { seq, text: typeof replyText === 'string' ? replyText : '', envelope };
|
|
@@ -227,18 +315,19 @@ export class CompanionClient {
|
|
|
227
315
|
* final envelope locally so onMessage fires immediately and the log replay
|
|
228
316
|
* dedups by id). Falls back to buffered semantics if the server ever
|
|
229
317
|
* responds with plain JSON (e.g. an old deployment). */
|
|
230
|
-
async sendTextStreaming(sessionId, body) {
|
|
318
|
+
async sendTextStreaming(sessionId, body, signal) {
|
|
231
319
|
const res = await this.fetchAuthed(this.url(`/api/companion/session/${sessionId}/input`), {
|
|
232
320
|
method: 'POST',
|
|
233
321
|
headers: this.jsonHeaders(),
|
|
234
|
-
body: JSON.stringify({ ...body, stream: true })
|
|
322
|
+
body: JSON.stringify({ ...body, stream: true }),
|
|
323
|
+
...(signal ? { signal } : {})
|
|
235
324
|
});
|
|
236
325
|
const ctype = res.headers.get('content-type') ?? '';
|
|
237
326
|
if (!ctype.includes('text/event-stream')) {
|
|
238
327
|
// Old server / error before the stream opened — behave like postJson.
|
|
239
328
|
const json = (await res.json().catch(() => null));
|
|
240
329
|
if (!res.ok || !json || json.ok === false) {
|
|
241
|
-
throw new CompanionError(json?.error || `request failed (${res.status})`, res.status, json?.code);
|
|
330
|
+
throw new CompanionError(json?.error || `request failed (${res.status})`, res.status, json?.code, this.retryAfterFrom(res, json));
|
|
242
331
|
}
|
|
243
332
|
return { seq: json.seq ?? null };
|
|
244
333
|
}
|
|
@@ -251,43 +340,80 @@ export class CompanionClient {
|
|
|
251
340
|
for (const h of this.deltaHandlers)
|
|
252
341
|
h(chunk, meta);
|
|
253
342
|
};
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
343
|
+
try {
|
|
344
|
+
for (;;) {
|
|
345
|
+
const { done, value } = await reader.read();
|
|
346
|
+
if (value)
|
|
347
|
+
buf += decoder.decode(value, { stream: true });
|
|
348
|
+
const { events, rest } = parseSse(buf);
|
|
349
|
+
buf = rest;
|
|
350
|
+
for (const ev of events) {
|
|
351
|
+
if (ev.event === 'delta') {
|
|
352
|
+
// A malformed delta frame is dropped, not fatal — losing one text
|
|
353
|
+
// chunk beats throwing away the whole turn (the final message
|
|
354
|
+
// arrives intact in the done frame / event stream regardless).
|
|
355
|
+
let d;
|
|
356
|
+
try {
|
|
357
|
+
d = JSON.parse(ev.data);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (typeof d.text === 'string' && d.text)
|
|
363
|
+
fireDelta(d.text, {});
|
|
273
364
|
}
|
|
274
|
-
|
|
275
|
-
|
|
365
|
+
else if (ev.event === 'reset') {
|
|
366
|
+
fireDelta('', { reset: true });
|
|
276
367
|
}
|
|
277
|
-
if (
|
|
278
|
-
|
|
368
|
+
else if (ev.event === 'done') {
|
|
369
|
+
// The done frame carries the turn's resolution — unparseable means
|
|
370
|
+
// the turn's outcome is unknowable, so fail it explicitly instead
|
|
371
|
+
// of letting a raw SyntaxError escape the SDK.
|
|
372
|
+
let d;
|
|
373
|
+
try {
|
|
374
|
+
d = JSON.parse(ev.data);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
try {
|
|
378
|
+
await reader.cancel();
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
/* stream already finished */
|
|
382
|
+
}
|
|
383
|
+
throw new CompanionError('malformed done frame', 502);
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
await reader.cancel();
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
/* stream already finished */
|
|
390
|
+
}
|
|
391
|
+
if (d.ok === false) {
|
|
392
|
+
throw new CompanionError(d.error || 'turn failed', d.status ?? 500, d.code);
|
|
393
|
+
}
|
|
394
|
+
// Emit the final envelope NOW so onMessage fires with zero poll
|
|
395
|
+
// latency; the event-stream replay of the same id is deduped.
|
|
396
|
+
if (d.envelope && typeof d.envelope === 'object')
|
|
397
|
+
this.emit(d.envelope);
|
|
398
|
+
return { seq: d.seq ?? null, envelope: d.envelope ?? undefined, kind: d.kind };
|
|
279
399
|
}
|
|
280
|
-
// Emit the final envelope NOW so onMessage fires with zero poll
|
|
281
|
-
// latency; the event-stream replay of the same id is deduped.
|
|
282
|
-
if (d.envelope && typeof d.envelope === 'object')
|
|
283
|
-
this.emit(d.envelope);
|
|
284
|
-
return { seq: d.seq ?? null, envelope: d.envelope ?? undefined };
|
|
285
400
|
}
|
|
401
|
+
if (done)
|
|
402
|
+
break;
|
|
286
403
|
}
|
|
287
|
-
|
|
288
|
-
|
|
404
|
+
throw new CompanionError('stream ended without a done frame', 502);
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
// A throwing user onDelta handler (or an abort) must not leak the
|
|
408
|
+
// reader — cancel so the connection is released, then rethrow.
|
|
409
|
+
try {
|
|
410
|
+
await reader.cancel();
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
/* stream already finished */
|
|
414
|
+
}
|
|
415
|
+
throw e;
|
|
289
416
|
}
|
|
290
|
-
throw new CompanionError('stream ended without a done frame', 502);
|
|
291
417
|
}
|
|
292
418
|
/** Subscribe to token-streaming deltas of the reply (P1-2). Registering a
|
|
293
419
|
* subscriber makes sendText stream automatically. Returns an unsubscribe
|
|
@@ -314,7 +440,14 @@ export class CompanionClient {
|
|
|
314
440
|
});
|
|
315
441
|
const body = Array.isArray(event) ? { events: event.map(fill) } : fill(event);
|
|
316
442
|
const json = await this.postJson(`/api/companion/session/${id}/context`, body);
|
|
317
|
-
|
|
443
|
+
// injected/reacted tell the host a live-call inject or a proactive text
|
|
444
|
+
// reaction was triggered by this batch (0.28.0 — previously discarded).
|
|
445
|
+
return {
|
|
446
|
+
accepted: json.accepted ?? 0,
|
|
447
|
+
dropped: json.dropped ?? 0,
|
|
448
|
+
injected: json.injected ?? 0,
|
|
449
|
+
reacted: json.reacted === true
|
|
450
|
+
};
|
|
318
451
|
}
|
|
319
452
|
randomId() {
|
|
320
453
|
const c = globalThis.crypto;
|
|
@@ -350,12 +483,36 @@ export class CompanionClient {
|
|
|
350
483
|
// the caller's own onTranscript.
|
|
351
484
|
const transcript = [];
|
|
352
485
|
const callerOnTranscript = opts?.onTranscript;
|
|
486
|
+
const callerOnError = opts?.onError;
|
|
487
|
+
// Cleanup must run whichever side ends the call — the app calling close()
|
|
488
|
+
// OR a provider-initiated teardown (pc failure / EL disconnect), which
|
|
489
|
+
// surfaces as onError with no close() following. Idempotent. `unsub` is
|
|
490
|
+
// assigned after the call opens; a connect-phase onError finds the no-op.
|
|
491
|
+
let unsub = () => { };
|
|
492
|
+
let cleaned = false;
|
|
493
|
+
const cleanup = () => {
|
|
494
|
+
if (cleaned)
|
|
495
|
+
return;
|
|
496
|
+
cleaned = true;
|
|
497
|
+
unsub();
|
|
498
|
+
// Consolidate this call into durable memory (best-effort, keepalive).
|
|
499
|
+
void this.endSession({ transcript });
|
|
500
|
+
};
|
|
353
501
|
const wrappedOpts = {
|
|
354
502
|
...opts,
|
|
355
503
|
onTranscript: (e) => {
|
|
356
|
-
|
|
504
|
+
// Cap the buffer (endSession consolidates the tail anyway) so an
|
|
505
|
+
// hours-long call can't grow this array unbounded.
|
|
506
|
+
if (e?.text?.trim()) {
|
|
357
507
|
transcript.push({ role: e.role, text: e.text });
|
|
508
|
+
if (transcript.length > 200)
|
|
509
|
+
transcript.splice(0, transcript.length - 200);
|
|
510
|
+
}
|
|
358
511
|
callerOnTranscript?.(e);
|
|
512
|
+
},
|
|
513
|
+
onError: (err) => {
|
|
514
|
+
cleanup();
|
|
515
|
+
callerOnError?.(err);
|
|
359
516
|
}
|
|
360
517
|
};
|
|
361
518
|
// Let the live voice call invoke tools — routed through the SAME
|
|
@@ -385,7 +542,7 @@ export class CompanionClient {
|
|
|
385
542
|
// Bridge server-pushed voiceRelevant moments into the live call (item 2):
|
|
386
543
|
// the server emits companion.voice_inject while the call is active; inject
|
|
387
544
|
// each into the session so the companion reacts out loud. Needs start().
|
|
388
|
-
|
|
545
|
+
unsub = this.on('companion.voice_inject', (env) => {
|
|
389
546
|
const p = env.payload;
|
|
390
547
|
if (typeof p?.text === 'string' && p.text)
|
|
391
548
|
call.injectEvent(p.text, p.speak !== false);
|
|
@@ -394,10 +551,8 @@ export class CompanionClient {
|
|
|
394
551
|
return {
|
|
395
552
|
...call,
|
|
396
553
|
close: () => {
|
|
397
|
-
unsub();
|
|
398
554
|
close();
|
|
399
|
-
|
|
400
|
-
void this.endSession({ transcript });
|
|
555
|
+
cleanup();
|
|
401
556
|
}
|
|
402
557
|
};
|
|
403
558
|
}
|
|
@@ -864,19 +1019,20 @@ export class CompanionClient {
|
|
|
864
1019
|
return;
|
|
865
1020
|
this.requireSession();
|
|
866
1021
|
this.streaming = true;
|
|
1022
|
+
const gen = ++this.streamGen;
|
|
867
1023
|
const wsImpl = this.opts.webSocketImpl ?? globalThis.WebSocket;
|
|
868
1024
|
if (this.opts.stream === 'websocket' && wsImpl) {
|
|
869
|
-
void this.wsLoop(wsImpl);
|
|
1025
|
+
void this.wsLoop(wsImpl, gen);
|
|
870
1026
|
}
|
|
871
1027
|
else {
|
|
872
|
-
void this.loop();
|
|
1028
|
+
void this.loop(gen);
|
|
873
1029
|
}
|
|
874
1030
|
}
|
|
875
1031
|
/** WebSocket receive loop. Resolves when the socket closes; on any failure
|
|
876
1032
|
* while still streaming it falls back to the SSE loop, so opting into WS can
|
|
877
1033
|
* only ever DEGRADE to SSE, never drop replies. Input is still sent over
|
|
878
1034
|
* REST (sendInput) — receiving is the latency-sensitive half. */
|
|
879
|
-
async wsLoop(WS) {
|
|
1035
|
+
async wsLoop(WS, gen) {
|
|
880
1036
|
const id = this.requireSession();
|
|
881
1037
|
const { deriveWsStreamUrl, decodeWsMessage } = await import('./ws-transport.js');
|
|
882
1038
|
// Reconnect across CLEAN closes (a WS gateway with a bounded lifetime, like
|
|
@@ -884,7 +1040,7 @@ export class CompanionClient {
|
|
|
884
1040
|
// must not silently end reply delivery. Only a connection ERROR degrades to
|
|
885
1041
|
// SSE, and only once. Each connection's poll timer + socket are released in
|
|
886
1042
|
// the settle() finally so neither leaks after fallback/reconnect.
|
|
887
|
-
while (this.streaming) {
|
|
1043
|
+
while (this.streaming && gen === this.streamGen) {
|
|
888
1044
|
let errored = false;
|
|
889
1045
|
try {
|
|
890
1046
|
await new Promise((resolve, reject) => {
|
|
@@ -911,7 +1067,7 @@ export class CompanionClient {
|
|
|
911
1067
|
resolve();
|
|
912
1068
|
};
|
|
913
1069
|
ws.onmessage = (ev) => {
|
|
914
|
-
if (!this.streaming) {
|
|
1070
|
+
if (!this.streaming || gen !== this.streamGen) {
|
|
915
1071
|
settle();
|
|
916
1072
|
return;
|
|
917
1073
|
}
|
|
@@ -924,7 +1080,7 @@ export class CompanionClient {
|
|
|
924
1080
|
ws.onclose = () => settle();
|
|
925
1081
|
// Stopping the stream closes the socket + clears this timer.
|
|
926
1082
|
poll = setInterval(() => {
|
|
927
|
-
if (!this.streaming)
|
|
1083
|
+
if (!this.streaming || gen !== this.streamGen)
|
|
928
1084
|
settle();
|
|
929
1085
|
}, 250);
|
|
930
1086
|
});
|
|
@@ -934,8 +1090,8 @@ export class CompanionClient {
|
|
|
934
1090
|
}
|
|
935
1091
|
if (errored) {
|
|
936
1092
|
// Socket unavailable / rejected — degrade to the robust SSE loop (once).
|
|
937
|
-
if (this.streaming)
|
|
938
|
-
await this.loop();
|
|
1093
|
+
if (this.streaming && gen === this.streamGen)
|
|
1094
|
+
await this.loop(gen);
|
|
939
1095
|
return;
|
|
940
1096
|
}
|
|
941
1097
|
// Clean close: the while-guard reconnects if still streaming, else exits.
|
|
@@ -944,6 +1100,7 @@ export class CompanionClient {
|
|
|
944
1100
|
/** Stop the event stream. */
|
|
945
1101
|
stop() {
|
|
946
1102
|
this.streaming = false;
|
|
1103
|
+
this.streamGen++; // invalidate any loop still unwinding
|
|
947
1104
|
this.abort?.abort();
|
|
948
1105
|
this.abort = null;
|
|
949
1106
|
}
|
|
@@ -1013,11 +1170,11 @@ export class CompanionClient {
|
|
|
1013
1170
|
if (env && typeof env.type === 'string')
|
|
1014
1171
|
this.emit(env);
|
|
1015
1172
|
}
|
|
1016
|
-
async loop() {
|
|
1173
|
+
async loop(gen) {
|
|
1017
1174
|
const id = this.requireSession();
|
|
1018
1175
|
let backoff = 500;
|
|
1019
1176
|
let authRefreshes = 0; // consecutive 401→refresh cycles without a healthy connect
|
|
1020
|
-
while (this.streaming) {
|
|
1177
|
+
while (this.streaming && gen === this.streamGen) {
|
|
1021
1178
|
this.abort = new AbortController();
|
|
1022
1179
|
try {
|
|
1023
1180
|
const res = await this.doFetch(this.url(`/api/companion/session/${id}/stream?cursor=${this.cursor}`), { headers: { Authorization: `Bearer ${this.opts.token}` }, signal: this.abort.signal });
|
|
@@ -1050,7 +1207,7 @@ export class CompanionClient {
|
|
|
1050
1207
|
break;
|
|
1051
1208
|
}
|
|
1052
1209
|
if (!res.ok || !res.body) {
|
|
1053
|
-
if (!this.streaming)
|
|
1210
|
+
if (!this.streaming || gen !== this.streamGen)
|
|
1054
1211
|
break;
|
|
1055
1212
|
await this.sleep(backoff);
|
|
1056
1213
|
backoff = Math.min(backoff * 2, 8000);
|
|
@@ -1058,21 +1215,21 @@ export class CompanionClient {
|
|
|
1058
1215
|
}
|
|
1059
1216
|
backoff = 500; // healthy connection resets backoff
|
|
1060
1217
|
authRefreshes = 0;
|
|
1061
|
-
await this.consume(res.body);
|
|
1218
|
+
await this.consume(res.body, gen);
|
|
1062
1219
|
}
|
|
1063
1220
|
catch {
|
|
1064
|
-
if (!this.streaming)
|
|
1221
|
+
if (!this.streaming || gen !== this.streamGen)
|
|
1065
1222
|
break;
|
|
1066
1223
|
await this.sleep(backoff);
|
|
1067
1224
|
backoff = Math.min(backoff * 2, 8000);
|
|
1068
1225
|
}
|
|
1069
1226
|
}
|
|
1070
1227
|
}
|
|
1071
|
-
async consume(body) {
|
|
1228
|
+
async consume(body, gen) {
|
|
1072
1229
|
const reader = body.getReader();
|
|
1073
1230
|
const decoder = new TextDecoder();
|
|
1074
1231
|
let buf = '';
|
|
1075
|
-
while (this.streaming) {
|
|
1232
|
+
while (this.streaming && gen === this.streamGen) {
|
|
1076
1233
|
const { done, value } = await reader.read();
|
|
1077
1234
|
if (done)
|
|
1078
1235
|
break;
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
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. */
|
|
7
|
+
/** `CompanionError.code` values: the server vocabulary (COMPANION_ERROR_CODES)
|
|
8
|
+
* plus the SDK-synthesized ones. The `(string & {})` arm keeps the type open
|
|
9
|
+
* for codes newer than this SDK build while preserving autocomplete. */
|
|
10
|
+
export type CompanionErrorCodeValue = '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' | 'confirm_not_found' | 'confirm_resolved' | 'step_up_required' | 'step_up_failed' | 'reply_timeout' | 'needs_event_stream' | 'not_connected' | 'missing_option' | (string & {});
|
|
5
11
|
export declare class CompanionError extends Error {
|
|
6
12
|
status: number;
|
|
7
|
-
code?:
|
|
8
|
-
|
|
13
|
+
code?: CompanionErrorCodeValue;
|
|
14
|
+
retryAfter?: number;
|
|
15
|
+
constructor(message: string, status: number, code?: string, retryAfter?: number);
|
|
9
16
|
}
|
package/dist/errors.js
CHANGED
|
@@ -3,18 +3,17 @@
|
|
|
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
|
-
/** 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
6
|
export class CompanionError extends Error {
|
|
11
7
|
status;
|
|
12
8
|
code;
|
|
13
|
-
|
|
9
|
+
retryAfter;
|
|
10
|
+
constructor(message, status, code, retryAfter) {
|
|
14
11
|
super(message);
|
|
15
12
|
this.name = 'CompanionError';
|
|
16
13
|
this.status = status;
|
|
17
14
|
if (code)
|
|
18
15
|
this.code = code;
|
|
16
|
+
if (typeof retryAfter === 'number' && Number.isFinite(retryAfter))
|
|
17
|
+
this.retryAfter = retryAfter;
|
|
19
18
|
}
|
|
20
19
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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';
|
|
3
4
|
export type { CompanionClientOptions, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, ToolCallEvent, WorldStateInput, CompanionAvatar, CompanionWallet, WalletBalance, BrandIconSize, EndSessionResult, SendTextReply } from './client.js';
|
|
4
5
|
export { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_VISUAL_TOOLS, AVATAR_VISUAL_TOOL_NAMES } from './call.js';
|
|
5
6
|
export type { CompanionCall, CompanionCallOptions, VoiceToolBridge } from './call.js';
|
|
6
7
|
export { COMPANION_SCOPES, COMPANION_MODALITIES, SENSITIVE_SCOPES, REPRESENT_SCOPES, DEFAULT_SCOPES, DEFAULT_MODALITIES, isSensitiveScope, isRepresentScope, hasScope } from './scopes.js';
|
|
7
8
|
export type { CompanionScope, CompanionModality } from './scopes.js';
|
|
8
|
-
export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, ToolCallPayload, AudioClipPayload, ExpressionPayload, TypingPayload, VoiceInjectPayload, UsagePayload } from './protocol.js';
|
|
9
|
+
export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, ToolCallPayload, AudioClipPayload, ExpressionPayload, TypingPayload, VoiceInjectPayload, UsagePayload, MessagePayload } from './protocol.js';
|
|
9
10
|
export { PROTOCOL_VERSION, INBOUND_TYPES, OUTBOUND_TYPES, COMPANION_ERROR_CODES } from './protocol.js';
|
|
10
11
|
export type { CompanionErrorCode } from './protocol.js';
|
|
11
12
|
/** Create a companion client. */
|
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", "confirm_not_found", "confirm_resolved", "step_up_required", "step_up_failed"];
|
|
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> {
|
|
@@ -163,6 +163,14 @@ export interface VoiceInjectPayload {
|
|
|
163
163
|
export interface TypingPayload {
|
|
164
164
|
active: boolean;
|
|
165
165
|
}
|
|
166
|
+
/** Payload of a `companion.message` event — the companion's text reply. When
|
|
167
|
+
* the turn was started by this client with sendText, `replyTo` echoes the
|
|
168
|
+
* request's turnId (0.28.0) so a reply can be correlated to its turn;
|
|
169
|
+
* proactive messages (world-state reactions, confirm outcomes) carry none. */
|
|
170
|
+
export interface MessagePayload {
|
|
171
|
+
text: string;
|
|
172
|
+
replyTo?: string;
|
|
173
|
+
}
|
|
166
174
|
/** A live world-state event the app streams in. `retained:true` = latest-value
|
|
167
175
|
* state (coalesced); `retained:false` = a transient moment carrying `salience`
|
|
168
176
|
* and optional `voiceRelevant`. */
|
package/dist/protocol.js
CHANGED
|
@@ -54,6 +54,11 @@ 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
|
-
'unavailable' // 503 — backing store/provider not configured or down
|
|
59
|
+
'unavailable', // 503 — backing store/provider not configured or down
|
|
60
|
+
'confirm_not_found', // 404 — confirmId unknown or not on this session
|
|
61
|
+
'confirm_resolved', // 409 — confirm already approved/denied/expired (single-use)
|
|
62
|
+
'step_up_required', // 401 — approval needs a passkey assertion (fetch confirm/stepup)
|
|
63
|
+
'step_up_failed' // 401 — the passkey assertion didn't verify; retry the step-up
|
|
59
64
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pouchy_ai/companion-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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",
|