@pouchy_ai/companion-sdk 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -12,6 +12,46 @@ a protocol bump is always called out explicitly here.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.18.1] - 2026-07-08
16
+
17
+ ### Changed
18
+
19
+ - `confirmAction()` return type widened: `status` may now be `'exec_failed'` and
20
+ the response carries an optional `retryable` flag. When an approved **idempotent**
21
+ action (a wallet payment) fails on a transient error, the server no longer burns
22
+ the confirmation — it returns `{ status: 'exec_failed', retryable: true }`, and
23
+ you MAY re-call `confirmAction(sameConfirmId, true)` to re-run it (the server
24
+ dedupes a partial first attempt on the confirmId). Non-idempotent actions (a
25
+ friend message, a skill POST) stay strictly single-use and are never retryable.
26
+ Additive + backwards-compatible: existing `'approved' | 'denied'` handling is
27
+ unaffected. No wire-protocol change.
28
+
29
+ ## [0.18.0] - 2026-07-08
30
+
31
+ ### Added
32
+
33
+ - **`close()`** — one-call teardown for a non-voice (text) session: stops the
34
+ event stream and calls `endSession()` once (idempotent). The text-path mirror
35
+ of what a `connectCall()` handle's `close()` already does for voice.
36
+ - **`EndSessionResult`** type — exported. `endSession()` now RETURNS the server's
37
+ consolidation diagnostic `{ ok, facts?, skipped? }` (or `null`) instead of
38
+ discarding it, so an integrator whose memory write silently does nothing (wrong
39
+ id → `skipped: 'no_session'`, empty transcript → `'no_content'`) gets the
40
+ signal the server already provides. Backwards-compatible: callers ignoring the
41
+ return value are unaffected (`connectCall`'s internal `void endSession()` too).
42
+ - **`CompanionError.code`** — an optional stable, machine-readable tag on the
43
+ thrown error. Local precondition mistakes now throw a `CompanionError`
44
+ (`status: 0`) instead of a bare `Error`, with codes `not_connected` (a method
45
+ called before `connect()`), `missing_option` (no `baseUrl`/`token`), and
46
+ `not_representative` (`pairVisitor` off a non-representative session); HTTP
47
+ failures carry the server's error `code` when present. `catch (e) { if (e
48
+ instanceof CompanionError) … }` — the natural one-error-type pattern — now
49
+ catches the common first-run mistakes too.
50
+
51
+ ### Changed
52
+
53
+ - `endSession(opts?)` return type: `Promise<void>` → `Promise<EndSessionResult | null>`.
54
+
15
55
  ## [0.17.0] - 2026-07-08
16
56
 
17
57
  ### Added
package/README.md CHANGED
@@ -18,6 +18,10 @@ 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
19
  Changes per release are tracked in [`CHANGELOG.md`](./CHANGELOG.md).
20
20
 
21
+ **React?** [`@pouchy_ai/react`](../react-sdk/README.md) wraps this SDK in a
22
+ `<CompanionProvider>` + `useCompanion` / `useMessages` / `useTyping` / `useCall`
23
+ hooks so you don't wire the handshake, stream, or teardown yourself.
24
+
21
25
  ## Quickstart
22
26
 
23
27
  Two steps. **1)** Your backend exchanges the project **Secret Key** (create one
package/dist/client.d.ts CHANGED
@@ -129,9 +129,25 @@ export type BrandIconSize = 256 | 512 | 1024;
129
129
  * deployment origin. Standalone (no client/token needed) so a surface can show
130
130
  * the Pouchy mark before/without connecting. */
131
131
  export declare function pouchyBrandIconUrl(baseUrl: string, size?: BrandIconSize): string;
132
+ /** The single error type the SDK throws — for HTTP failures (`status` is the
133
+ * response code) AND for local precondition mistakes (`status: 0`, e.g. calling
134
+ * a method before `connect()`). `code` is a stable machine-readable tag you can
135
+ * switch on, shared with the `control.error` stream event vocabulary:
136
+ * `'not_connected'`, `'missing_option'`, `'not_representative'`, or the server's
137
+ * own error code for an HTTP failure (when it sends one). */
132
138
  export declare class CompanionError extends Error {
133
139
  status: number;
134
- constructor(message: string, status: number);
140
+ code?: string;
141
+ constructor(message: string, status: number, code?: string);
142
+ }
143
+ /** The server's memory-consolidation diagnostic from `endSession()` / `close()`.
144
+ * `ok` is whether consolidation ran; `facts` is how many memories it wrote;
145
+ * `skipped` (when present) says why nothing was written
146
+ * (`'no_session'` | `'no_content'` | `'throttled'`). */
147
+ export interface EndSessionResult {
148
+ ok: boolean;
149
+ facts?: number;
150
+ skipped?: string;
135
151
  }
136
152
  type Handler = (envelope: CompanionEnvelope) => void;
137
153
  /** Token-streaming subscriber (P1-2). `chunk` is a raw text delta of the reply
@@ -242,13 +258,27 @@ export declare class CompanionClient {
242
258
  * Called automatically when a connectCall() handle is closed; call it directly
243
259
  * when a non-voice session ends. Best-effort + idempotent (safe to call twice;
244
260
  * uses keepalive so it survives a page unload). Pass the voice transcript when
245
- * you have it (connectCall does this for you). */
261
+ * you have it (connectCall does this for you).
262
+ *
263
+ * Returns the server's consolidation diagnostic — `{ ok, facts?, skipped? }`
264
+ * — or `null` if there was no session or the (swallowed) keepalive request
265
+ * failed. `skipped` tells you WHY nothing was written: `'no_session'` (you
266
+ * passed the instance id, not the session id), `'no_content'` (empty turn log
267
+ * / a voice transcript that never arrived), `'throttled'` (a duplicate end
268
+ * beacon — harmless). `facts` is how many memories were consolidated. */
246
269
  endSession(opts?: {
247
270
  transcript?: {
248
271
  role: string;
249
272
  text: string;
250
273
  }[];
251
- }): Promise<void>;
274
+ }): Promise<EndSessionResult | null>;
275
+ /** Tear the session down in one call: stop the event stream and consolidate
276
+ * memory (endSession) exactly once. The single teardown entry point for a
277
+ * NON-voice (text) session — mirror of what a connectCall() handle's close()
278
+ * does for voice. Idempotent. Returns endSession's diagnostic (or null).
279
+ * NOTE: if you drive `ping()` on your own timer for a background tab, clear
280
+ * that timer yourself — the client doesn't own it. */
281
+ close(): Promise<EndSessionResult | null>;
252
282
  /** Report the result of a companion.tool_call this surface performed. When all
253
283
  * of the turn's calls are reported, the companion resumes and the continuation
254
284
  * (a companion.message or more tool calls) arrives on the event stream. */
@@ -408,12 +438,19 @@ export declare class CompanionClient {
408
438
  * approval the recorded action runs server-side; its result comes back as
409
439
  * `outcome` in this response (render it where the user tapped) AND as a
410
440
  * normal companion.message on the stream (so the conversation shows it).
411
- * A denial returns/emits a brief decline the same way. One-time:
412
- * re-resolving a settled confirmId fails with 409. */
441
+ * A denial returns/emits a brief decline the same way. Single-use:
442
+ * re-resolving a settled confirmId fails with 409.
443
+ *
444
+ * RETRY: if an approved IDEMPOTENT action (a wallet payment) flakes on a
445
+ * transient error, `status` is `'exec_failed'` and `retryable` is true — you
446
+ * MAY call `confirmAction(sameConfirmId, true)` again to re-run it (the server
447
+ * dedupes a partial first attempt). A non-idempotent action (a message / skill)
448
+ * stays terminal and is never retryable. */
413
449
  confirmAction(confirmId: string, approve: boolean): Promise<{
414
450
  ok: boolean;
415
- status: 'approved' | 'denied';
451
+ status: 'approved' | 'denied' | 'exec_failed';
416
452
  outcome?: string;
453
+ retryable?: boolean;
417
454
  }>;
418
455
  /** The session's still-pending confirmations (display-safe: id, summary,
419
456
  * scope, timestamps — never raw args). Use it to rebuild your confirm card
package/dist/client.js CHANGED
@@ -19,12 +19,21 @@ import { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_
19
19
  export function pouchyBrandIconUrl(baseUrl, size = 512) {
20
20
  return baseUrl.replace(/\/+$/, '') + `/brand-assets/icon/pouchy-icon-${size}.png`;
21
21
  }
22
+ /** The single error type the SDK throws — for HTTP failures (`status` is the
23
+ * response code) AND for local precondition mistakes (`status: 0`, e.g. calling
24
+ * a method before `connect()`). `code` is a stable machine-readable tag you can
25
+ * switch on, shared with the `control.error` stream event vocabulary:
26
+ * `'not_connected'`, `'missing_option'`, `'not_representative'`, or the server's
27
+ * own error code for an HTTP failure (when it sends one). */
22
28
  export class CompanionError extends Error {
23
29
  status;
24
- constructor(message, status) {
30
+ code;
31
+ constructor(message, status, code) {
25
32
  super(message);
26
33
  this.name = 'CompanionError';
27
34
  this.status = status;
35
+ if (code)
36
+ this.code = code;
28
37
  }
29
38
  }
30
39
  const SEEN_CAP = 512;
@@ -44,9 +53,9 @@ export class CompanionClient {
44
53
  pendingVoiceTools = new Map();
45
54
  constructor(opts) {
46
55
  if (!opts.baseUrl)
47
- throw new Error('CompanionClient: baseUrl is required');
56
+ throw new CompanionError('CompanionClient: baseUrl is required', 0, 'missing_option');
48
57
  if (!opts.token)
49
- throw new Error('CompanionClient: token is required');
58
+ throw new CompanionError('CompanionClient: token is required', 0, 'missing_option');
50
59
  this.opts = opts;
51
60
  this.doFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
52
61
  }
@@ -62,7 +71,7 @@ export class CompanionClient {
62
71
  }
63
72
  requireSession() {
64
73
  if (!this._session)
65
- throw new Error('CompanionClient: call connect() first');
74
+ throw new CompanionError('CompanionClient: call connect() first', 0, 'not_connected');
66
75
  return this._session;
67
76
  }
68
77
  async postJson(path, body) {
@@ -73,7 +82,7 @@ export class CompanionClient {
73
82
  });
74
83
  const json = (await res.json().catch(() => null));
75
84
  if (!res.ok || !json || json.ok === false) {
76
- throw new CompanionError(json?.error || `request failed (${res.status})`, res.status);
85
+ throw new CompanionError(json?.error || `request failed (${res.status})`, res.status, json?.code);
77
86
  }
78
87
  return json;
79
88
  }
@@ -106,10 +115,10 @@ export class CompanionClient {
106
115
  * — as proof + consent. The visitor must therefore also be a Pouchy user. */
107
116
  async pairVisitor(visitorToken) {
108
117
  if (!this.opts.visitor) {
109
- throw new Error('pairVisitor() requires a representative session (createCompanion({ visitor }))');
118
+ throw new CompanionError('pairVisitor() requires a representative session (createCompanion({ visitor }))', 0, 'not_representative');
110
119
  }
111
120
  if (!visitorToken)
112
- throw new Error('pairVisitor: visitorToken is required');
121
+ throw new CompanionError('pairVisitor: visitorToken is required', 0, 'missing_option');
113
122
  const json = await this.postJson('/api/companion/pair', {
114
123
  visitorToken,
115
124
  visitorId: this.opts.visitor.id
@@ -320,28 +329,47 @@ export class CompanionClient {
320
329
  * Called automatically when a connectCall() handle is closed; call it directly
321
330
  * when a non-voice session ends. Best-effort + idempotent (safe to call twice;
322
331
  * uses keepalive so it survives a page unload). Pass the voice transcript when
323
- * you have it (connectCall does this for you). */
332
+ * you have it (connectCall does this for you).
333
+ *
334
+ * Returns the server's consolidation diagnostic — `{ ok, facts?, skipped? }`
335
+ * — or `null` if there was no session or the (swallowed) keepalive request
336
+ * failed. `skipped` tells you WHY nothing was written: `'no_session'` (you
337
+ * passed the instance id, not the session id), `'no_content'` (empty turn log
338
+ * / a voice transcript that never arrived), `'throttled'` (a duplicate end
339
+ * beacon — harmless). `facts` is how many memories were consolidated. */
324
340
  async endSession(opts) {
325
341
  const id = this._session;
326
342
  if (!id)
327
- return;
343
+ return null;
328
344
  // Cap the payload (fetch keepalive bodies are size-limited): last 60 lines.
329
345
  const transcript = (opts?.transcript ?? [])
330
346
  .filter((t) => t && typeof t.text === 'string' && t.text.trim())
331
347
  .slice(-60)
332
348
  .map((t) => ({ role: t.role, text: t.text.slice(0, 500) }));
333
349
  try {
334
- await this.doFetch(this.url(`/api/companion/session/${id}/end`), {
350
+ const res = await this.doFetch(this.url(`/api/companion/session/${id}/end`), {
335
351
  method: 'POST',
336
352
  headers: this.jsonHeaders(),
337
353
  body: JSON.stringify({ transcript }),
338
354
  keepalive: true
339
355
  });
356
+ return (await res.json().catch(() => null));
340
357
  }
341
358
  catch {
342
359
  /* best-effort — memory consolidation is not on the user's critical path */
360
+ return null;
343
361
  }
344
362
  }
363
+ /** Tear the session down in one call: stop the event stream and consolidate
364
+ * memory (endSession) exactly once. The single teardown entry point for a
365
+ * NON-voice (text) session — mirror of what a connectCall() handle's close()
366
+ * does for voice. Idempotent. Returns endSession's diagnostic (or null).
367
+ * NOTE: if you drive `ping()` on your own timer for a background tab, clear
368
+ * that timer yourself — the client doesn't own it. */
369
+ async close() {
370
+ this.stop();
371
+ return this.endSession();
372
+ }
345
373
  /** Report the result of a companion.tool_call this surface performed. When all
346
374
  * of the turn's calls are reported, the companion resumes and the continuation
347
375
  * (a companion.message or more tool calls) arrives on the event stream. */
@@ -650,8 +678,14 @@ export class CompanionClient {
650
678
  * approval the recorded action runs server-side; its result comes back as
651
679
  * `outcome` in this response (render it where the user tapped) AND as a
652
680
  * normal companion.message on the stream (so the conversation shows it).
653
- * A denial returns/emits a brief decline the same way. One-time:
654
- * re-resolving a settled confirmId fails with 409. */
681
+ * A denial returns/emits a brief decline the same way. Single-use:
682
+ * re-resolving a settled confirmId fails with 409.
683
+ *
684
+ * RETRY: if an approved IDEMPOTENT action (a wallet payment) flakes on a
685
+ * transient error, `status` is `'exec_failed'` and `retryable` is true — you
686
+ * MAY call `confirmAction(sameConfirmId, true)` again to re-run it (the server
687
+ * dedupes a partial first attempt). A non-idempotent action (a message / skill)
688
+ * stays terminal and is never retryable. */
655
689
  async confirmAction(confirmId, approve) {
656
690
  const sessionId = this.requireSession();
657
691
  return this.postJson(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`, { confirmId, approve });
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CompanionClient, type CompanionClientOptions } from './client.js';
2
2
  export { CompanionClient, CompanionError, pouchyBrandIconUrl } from './client.js';
3
- export type { CompanionClientOptions, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, WorldStateInput, CompanionAvatar, BrandIconSize } from './client.js';
3
+ export type { CompanionClientOptions, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, WorldStateInput, CompanionAvatar, BrandIconSize, EndSessionResult } from './client.js';
4
4
  export { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_VISUAL_TOOLS, AVATAR_VISUAL_TOOL_NAMES } from './call.js';
5
5
  export type { CompanionCall, CompanionCallOptions, VoiceToolBridge } from './call.js';
6
6
  export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, AudioClipPayload, ExpressionPayload, TypingPayload, VoiceInjectPayload, UsagePayload } from './protocol.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/companion-sdk",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
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",