@pouchy_ai/companion-sdk 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -12,7 +12,38 @@ a protocol bump is always called out explicitly here.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
- ## [0.15.0] - 2026-07-08
15
+ ## [0.17.0] - 2026-07-08
16
+
17
+ ### Added
18
+
19
+ Surface completion — the last three wire types that lacked a typed
20
+ client accessor now have one. All additive; wire protocol unchanged
21
+ (`PROTOCOL_VERSION 1`).
22
+
23
+ - **`onVoiceInject(fn)`** + `VoiceInjectPayload { text, speak }` — a typed
24
+ subscription for `companion.voice_inject` (a `voiceRelevant` world-state
25
+ line the companion should say aloud during a live call). The event was
26
+ already emitted; this is the public handler + payload type for it.
27
+ - **`client.setModalities(modalities)`** — change a live session's I/O
28
+ modalities mid-session (e.g. toggle `voice`). Intersected with the
29
+ token's granted modalities server-side; returns the effective set.
30
+ Backed by `POST /api/companion/session/{id}/modalities`.
31
+ - **`client.ping()`** — keepalive that bumps the session's last-seen time
32
+ so a long-idle embed stays "live" within the TTL (cross-app A2A friend
33
+ messages keep reaching it). Backed by
34
+ `POST /api/companion/session/{id}/ping`.
35
+
36
+ ### Added
37
+
38
+ - **Conversation history** — `client.history({ limit })` fetches this session's
39
+ recent turns (`{ user, assistant, ts }`, oldest→newest) so a reconnecting
40
+ embed can restore its transcript. Distinct from `recall` (durable memory /
41
+ facts): this is the raw exchange log. Reads the token's OWN session only
42
+ (tenant-safe by construction — the turn log is keyed by the token's uid);
43
+ internal per-turn trace (`meta`) is stripped server-side. `limit` defaults to
44
+ 20, capped at 50. New type `CompanionTurn`. Backed by
45
+ `GET /api/companion/session/{sessionId}/history`. Wire protocol unchanged
46
+ (`PROTOCOL_VERSION 1`) — this is a REST method, not a stream event.
16
47
 
17
48
  ### Added
18
49
 
package/README.md CHANGED
@@ -70,6 +70,7 @@ Subscribe with `on(type, fn)` (or `'*'`), or these typed convenience helpers:
70
70
  | `onConfirmRequest(fn)` | `companion.confirm_request` | a sensitive-op approval request. **Platform session tokens** (your end users): show your own confirm card and resolve it with `confirmAction` — this is how confirm-gated custom skills (POST / credentialed) run. First-party user tokens: observe-only — approval stays first-party (where the `stepUp:true` biometric gate lives) |
71
71
  | `onAudio(fn)` | `companion.audio` | TTS clip (non-call modality) |
72
72
  | `onExpression(fn)` | `companion.expression` | avatar viseme / expression / gesture |
73
+ | `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` |
73
74
  | `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 |
74
75
  | `onUsage(fn)` | `control.usage` | per-token metering echo |
75
76
  | `onError(fn)` | `control.error` | agent / stream errors |
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AudioClipPayload, CompanionEnvelope, ConfirmRequestPayload, ExpressionPayload, InterfaceUpdatePayload, OutboundType, PendingConfirm, RenderInterfacePayload, SocialMessagePayload, TypingPayload, UsagePayload, WorldStateEvent } from './protocol.js';
1
+ import type { AudioClipPayload, CompanionEnvelope, ConfirmRequestPayload, ExpressionPayload, InterfaceUpdatePayload, OutboundType, PendingConfirm, RenderInterfacePayload, SocialMessagePayload, TypingPayload, UsagePayload, VoiceInjectPayload, WorldStateEvent } from './protocol.js';
2
2
  /** Ergonomic world-state input: a WorldStateEvent with the CloudEvents plumbing
3
3
  * (specversion / id / source) optional — sendWorldState fills them. */
4
4
  export type WorldStateInput<D = unknown> = Omit<WorldStateEvent<D>, 'specversion' | 'id' | 'source'> & Partial<Pick<WorldStateEvent<D>, 'specversion' | 'id' | 'source'>>;
@@ -100,6 +100,14 @@ export interface RecalledMemory {
100
100
  namespace: string;
101
101
  createdAt?: string;
102
102
  }
103
+ /** One exchange from a session's conversation log (see `history`). `user` is the
104
+ * end-user's message, `assistant` the companion's reply; either may be `''` for
105
+ * a paused / assistant-only row. `ts` is epoch ms. */
106
+ export interface CompanionTurn {
107
+ user: string;
108
+ assistant: string;
109
+ ts: number;
110
+ }
103
111
  /** The user's current companion avatar (see getAvatar). */
104
112
  export interface CompanionAvatar {
105
113
  /** Companion display name (the user's chosen name), or null. */
@@ -273,6 +281,25 @@ export declare class CompanionClient {
273
281
  recall(opts?: {
274
282
  limit?: number;
275
283
  }): Promise<RecalledMemory[]>;
284
+ /** Fetch this session's recent conversation turns (oldest→newest) so a
285
+ * reconnecting embed can restore its transcript. Distinct from `recall`
286
+ * (durable memory / facts) — this is the raw exchange log. Returns the
287
+ * token's OWN session turns only; requires a live session (call `connect`
288
+ * first). `limit` defaults to 20, capped at 50. */
289
+ history(opts?: {
290
+ limit?: number;
291
+ }): Promise<CompanionTurn[]>;
292
+ /** Change this session's I/O modalities mid-session (e.g. enable/disable
293
+ * `voice`). The request is intersected with the token's granted modalities
294
+ * server-side — a session can't widen past its key — and the EFFECTIVE set is
295
+ * returned. Requires a live session. */
296
+ setModalities(modalities: string[]): Promise<{
297
+ modalities: string[];
298
+ }>;
299
+ /** Keepalive: bump this session's last-seen time so a long-idle embed stays
300
+ * "live" within the session TTL (cross-app A2A friend messages keep reaching
301
+ * it). Cheap; call it on a timer for a background tab. Requires a live session. */
302
+ ping(): Promise<void>;
276
303
  /** Remember a fact (into this app's namespace unless `namespace` is given and
277
304
  * the token has the core scope). Intimate-tier writes are rejected server-side. */
278
305
  remember(fact: {
@@ -407,6 +434,11 @@ export declare class CompanionClient {
407
434
  * tool-loop / thinking phase before the first text delta. Drive a "typing…"
408
435
  * affordance from it: `client.onTyping(({ active }) => setTyping(active))`. */
409
436
  onTyping(handler: (payload: TypingPayload, envelope: CompanionEnvelope) => void): () => void;
437
+ /** Convenience: subscribe to voice-inject cues (companion.voice_inject) — a
438
+ * `voiceRelevant` world-state line the companion should say aloud during a
439
+ * live call. Route `payload.text` to your active voice session when
440
+ * `payload.speak`. A text-only host can ignore it. */
441
+ onVoiceInject(handler: (payload: VoiceInjectPayload, envelope: CompanionEnvelope) => void): () => void;
410
442
  /** Open the event stream (auto-reconnecting with the resume cursor). Uses the
411
443
  * WebSocket plane when opted in + available, else SSE. */
412
444
  start(): void;
package/dist/client.js CHANGED
@@ -465,6 +465,52 @@ export class CompanionClient {
465
465
  }
466
466
  return json.memories ?? [];
467
467
  }
468
+ /** Fetch this session's recent conversation turns (oldest→newest) so a
469
+ * reconnecting embed can restore its transcript. Distinct from `recall`
470
+ * (durable memory / facts) — this is the raw exchange log. Returns the
471
+ * token's OWN session turns only; requires a live session (call `connect`
472
+ * first). `limit` defaults to 20, capped at 50. */
473
+ async history(opts) {
474
+ const id = this.requireSession();
475
+ const qs = opts?.limit ? `?limit=${encodeURIComponent(opts.limit)}` : '';
476
+ const res = await this.doFetch(this.url(`/api/companion/session/${encodeURIComponent(id)}/history${qs}`), { headers: { Authorization: `Bearer ${this.opts.token}` } });
477
+ const json = (await res.json().catch(() => null));
478
+ if (!res.ok || !json || json.ok === false) {
479
+ throw new CompanionError(json?.error || `history failed (${res.status})`, res.status);
480
+ }
481
+ return json.history ?? [];
482
+ }
483
+ /** Change this session's I/O modalities mid-session (e.g. enable/disable
484
+ * `voice`). The request is intersected with the token's granted modalities
485
+ * server-side — a session can't widen past its key — and the EFFECTIVE set is
486
+ * returned. Requires a live session. */
487
+ async setModalities(modalities) {
488
+ const id = this.requireSession();
489
+ const res = await this.doFetch(this.url(`/api/companion/session/${encodeURIComponent(id)}/modalities`), {
490
+ method: 'POST',
491
+ headers: {
492
+ Authorization: `Bearer ${this.opts.token}`,
493
+ 'Content-Type': 'application/json'
494
+ },
495
+ body: JSON.stringify({ modalities })
496
+ });
497
+ const json = (await res.json().catch(() => null));
498
+ if (!res.ok || !json || json.ok === false) {
499
+ throw new CompanionError(json?.error || `setModalities failed (${res.status})`, res.status);
500
+ }
501
+ return { modalities: json.modalities ?? modalities };
502
+ }
503
+ /** Keepalive: bump this session's last-seen time so a long-idle embed stays
504
+ * "live" within the session TTL (cross-app A2A friend messages keep reaching
505
+ * it). Cheap; call it on a timer for a background tab. Requires a live session. */
506
+ async ping() {
507
+ const id = this.requireSession();
508
+ const res = await this.doFetch(this.url(`/api/companion/session/${encodeURIComponent(id)}/ping`), { method: 'POST', headers: { Authorization: `Bearer ${this.opts.token}` } });
509
+ if (!res.ok) {
510
+ const json = (await res.json().catch(() => null));
511
+ throw new CompanionError(json?.error || `ping failed (${res.status})`, res.status);
512
+ }
513
+ }
468
514
  /** Remember a fact (into this app's namespace unless `namespace` is given and
469
515
  * the token has the core scope). Intimate-tier writes are rejected server-side. */
470
516
  async remember(fact) {
@@ -655,6 +701,17 @@ export class CompanionClient {
655
701
  handler(p, env);
656
702
  });
657
703
  }
704
+ /** Convenience: subscribe to voice-inject cues (companion.voice_inject) — a
705
+ * `voiceRelevant` world-state line the companion should say aloud during a
706
+ * live call. Route `payload.text` to your active voice session when
707
+ * `payload.speak`. A text-only host can ignore it. */
708
+ onVoiceInject(handler) {
709
+ return this.on('companion.voice_inject', (env) => {
710
+ const p = env.payload;
711
+ if (p && typeof p === 'object' && typeof p.text === 'string')
712
+ handler(p, env);
713
+ });
714
+ }
658
715
  /** Open the event stream (auto-reconnecting with the resume cursor). Uses the
659
716
  * WebSocket plane when opted in + available, else SSE. */
660
717
  start() {
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { CompanionClient, type CompanionClientOptions } from './client.js';
2
2
  export { CompanionClient, CompanionError, pouchyBrandIconUrl } from './client.js';
3
- export type { CompanionClientOptions, HelloAck, RecalledMemory, CallCredentials, CompanionToolDecl, WorldStateInput, CompanionAvatar, BrandIconSize } from './client.js';
3
+ export type { CompanionClientOptions, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, WorldStateInput, CompanionAvatar, BrandIconSize } 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
- export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, AudioClipPayload, ExpressionPayload, TypingPayload, UsagePayload } from './protocol.js';
6
+ export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, AudioClipPayload, ExpressionPayload, TypingPayload, VoiceInjectPayload, UsagePayload } from './protocol.js';
7
7
  /** Create a companion client. */
8
8
  export declare function createCompanion(opts: CompanionClientOptions): CompanionClient;
@@ -120,6 +120,15 @@ export interface SocialMessagePayload {
120
120
  /** ISO timestamp of delivery. */
121
121
  createdAt: string;
122
122
  }
123
+ /** Payload of a `companion.voice_inject` event — a `voiceRelevant` world-state
124
+ * line the companion should say aloud DURING a live call (the app pushed the
125
+ * moment via `sendWorldState`; the server decided it's worth voicing). `speak`
126
+ * is the host's cue to route `text` to the active voice session. A text-only
127
+ * host can ignore it — the moment still shaped the next turn's context. */
128
+ export interface VoiceInjectPayload {
129
+ text: string;
130
+ speak: boolean;
131
+ }
123
132
  /** Payload of a `companion.typing` event — an activity indicator bracketing the
124
133
  * model work of a turn: `active:true` when the companion starts working,
125
134
  * `active:false` when it emits its reply or pauses for an app tool result. It
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/companion-sdk",
3
- "version": "0.15.0",
3
+ "version": "0.17.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",