@pouchy_ai/companion-sdk 0.20.0 → 0.22.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,6 +12,53 @@ a protocol bump is always called out explicitly here.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.22.0] - 2026-07-11
16
+
17
+ ### Added
18
+
19
+ - **Runtime protocol constants are now exported.** `PROTOCOL_VERSION`,
20
+ `INBOUND_TYPES`, and `OUTBOUND_TYPES` are re-exported from the package entry —
21
+ previously only their *types* were reachable. A host can now assert the
22
+ protocol version or enumerate/validate the event vocabulary at runtime without
23
+ hard-coding the strings.
24
+ - **`ToolCallPayload` type.** The `companion.tool_call` payload
25
+ (`{ id, name, args }`) now has a named, exported interface, so a consumer using
26
+ the generic `on('companion.tool_call', …)` gets a typed payload — parity with
27
+ `RenderInterfacePayload`, `ConfirmRequestPayload`, and the other payload types.
28
+ `onToolCall`'s callback is typed against it.
29
+
30
+ No wire-protocol change (`PROTOCOL_VERSION` stays `1`); this is a purely additive
31
+ type/export surface, hence the minor bump.
32
+
33
+ ## [0.21.0] - 2026-07-11
34
+
35
+ ### Added
36
+
37
+ - **Token refresh:** `client.setToken(token)` swaps the bearer used by every
38
+ subsequent request and stream reconnect, and the new `onAuthError` client
39
+ option refreshes on demand — a 401 (REST **or** event stream) calls it; return
40
+ a fresh token (e.g. re-minted via `POST /v1/sessions`) and the client retries
41
+ transparently, return `null` to surface the failure exactly as before.
42
+ Concurrent 401s share one refresh; the stream loop caps consecutive
43
+ refresh-and-reject cycles at 3 so a bad hook can't hot-loop. Previously a
44
+ 1-hour session token expiring mid-embed silently stopped reply delivery
45
+ (`stream_unauthorized`) with no recovery short of rebuilding the client.
46
+ - **Semantic recall:** `recall({ query })` — the server has always supported
47
+ `?q=` semantic search over the token-visible facts; the client now exposes it.
48
+ Without `query` the ranking is importance/recency, as before.
49
+ - **Machine-readable error codes:** companion API error responses now carry a
50
+ stable `code` (`missing_token` / `invalid_token` / `missing_scope` /
51
+ `invalid_request` / `session_not_found` / `turn_pending` / `no_pending_tools` /
52
+ `unknown_call` / `payload_too_large` / `forbidden` / `unavailable`), so
53
+ `CompanionError.code` is finally switchable for HTTP failures — the type
54
+ always promised it; the server now delivers it. The vocabulary is append-only.
55
+
56
+ ### Fixed
57
+
58
+ - README method table showed pre-0.18 signatures for `recall` / `remember` /
59
+ `ingestKnowledge` / `history` (positional args instead of the actual object
60
+ args). Corrected to match the shipped API.
61
+
15
62
  ## [0.20.0] - 2026-07-11
16
63
 
17
64
  ### Added
package/README.md CHANGED
@@ -96,11 +96,12 @@ local/device skills).
96
96
 
97
97
  | Method | What it does |
98
98
  |---|---|
99
- | `recall(query)` / `remember(text)` | Read / write the token-scoped memory namespace. |
100
- | `ingestKnowledge(text, opts?)` / `ingestFile(file)` | Push documents into the user's knowledge base ("My materials"); needs `memory.write:core`. |
101
- | `history(limit?)` | Fetch the session transcript — restore chat on reload. |
99
+ | `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. |
100
+ | `ingestKnowledge({ text, name, … })` / `ingestFile(file)` | Push documents into the user's knowledge base ("My materials"); needs `memory.write:core`. |
101
+ | `history({ limit? })` | Fetch the session transcript — restore chat on reload. |
102
102
  | `setModalities([...])` | Switch active I/O mid-session (within the token's grant). |
103
103
  | `ping()` | Keepalive for long-idle embeds. |
104
+ | `setToken(token)` | Swap the bearer for every subsequent request + stream reconnect — session tokens expire (1h default), so call this when your backend re-mints one. Prefer the `onAuthError` constructor option to refresh on demand instead. |
104
105
  | `pendingConfirms()` / `confirmAction(id, approve)` | List / resolve pending sensitive-action confirms (platform session tokens). On `{ status: 'exec_failed', retryable: true }` you MAY re-call the same confirmId — idempotent actions only. |
105
106
  | `endSession()` | Distill the session into durable memory now; returns `{ skipped?: 'no_session' \| 'no_content' \| 'throttled' }`. |
106
107
  | `close()` | One-call teardown: `stop()` + `endSession()` — the text-session mirror of the call handle's `close()`. |
@@ -139,6 +140,12 @@ exported: `COMPANION_MODALITIES`, `REPRESENT_SCOPES`, `DEFAULT_MODALITIES`,
139
140
  `isRepresentScope()`. The list mirrors the server vocabulary and is guarded by
140
141
  the SDK's drift test.
141
142
 
143
+ The wire-protocol runtime constants are exported too — `PROTOCOL_VERSION`,
144
+ `INBOUND_TYPES`, and `OUTBOUND_TYPES` — so you can assert the version or
145
+ enumerate/validate the event vocabulary without hard-coding strings. Every
146
+ outbound payload has a named type; `ToolCallPayload` (`{ id, name, args }`) types
147
+ the `companion.tool_call` event and `onToolCall`'s callback.
148
+
142
149
  ## Representative mode (代聊)
143
150
 
144
151
  Pass a `visitor` and the session flips from **owner-facing** to **representative**:
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AudioClipPayload, CompanionEnvelope, ConfirmRequestPayload, ExpressionPayload, InterfaceUpdatePayload, OutboundType, PendingConfirm, RenderInterfacePayload, SocialMessagePayload, TypingPayload, UsagePayload, VoiceInjectPayload, WorldStateEvent } from './protocol.js';
1
+ import type { AudioClipPayload, CompanionEnvelope, ConfirmRequestPayload, ExpressionPayload, InterfaceUpdatePayload, OutboundType, PendingConfirm, RenderInterfacePayload, SocialMessagePayload, ToolCallPayload, 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'>>;
@@ -47,6 +47,14 @@ export interface CompanionClientOptions {
47
47
  /** Injectable WebSocket constructor (Node without a global WebSocket, or
48
48
  * tests). Defaults to globalThis.WebSocket when present. */
49
49
  webSocketImpl?: typeof WebSocket;
50
+ /** Called when a request or the event stream is rejected with 401 (expired /
51
+ * revoked token — session tokens live 1h by default). Return a fresh token
52
+ * (e.g. re-minted by your backend via POST /v1/sessions) and the client
53
+ * retries transparently; return null to give up, surfacing the failure
54
+ * exactly as without this hook. Scope denials (403 `missing_scope`) are
55
+ * configuration errors and never trigger it. Alternative: call `setToken()`
56
+ * proactively on your own refresh schedule. */
57
+ onAuthError?: () => string | null | Promise<string | null>;
50
58
  }
51
59
  export interface HelloAck {
52
60
  session: string;
@@ -190,6 +198,19 @@ export declare class CompanionClient {
190
198
  constructor(opts: CompanionClientOptions);
191
199
  /** The active session id, or null before connect(). */
192
200
  get sessionId(): string | null;
201
+ /** Swap the bearer token used by every subsequent request and stream
202
+ * reconnect. Session tokens expire (default 1h) — call this when your
203
+ * backend re-mints one, or wire `onAuthError` to do it on demand. */
204
+ setToken(token: string): void;
205
+ private refreshing;
206
+ /** Run opts.onAuthError (deduped), apply the fresh token, and report whether
207
+ * the caller should retry. Never throws — a failed refresh means "surface
208
+ * the original 401". */
209
+ private tryRefreshToken;
210
+ /** doFetch with one transparent retry after a 401-triggered token refresh.
211
+ * Everything except the stream loop (which has its own auth handling) goes
212
+ * through here. */
213
+ private fetchAuthed;
193
214
  private url;
194
215
  private jsonHeaders;
195
216
  private requireSession;
@@ -312,11 +333,7 @@ export declare class CompanionClient {
312
333
  * hang the call. */
313
334
  private invokeVoiceTool;
314
335
  /** Convenience: subscribe to tool-call requests from the companion. */
315
- onToolCall(handler: (call: {
316
- id: string;
317
- name: string;
318
- args: string;
319
- }, envelope: CompanionEnvelope) => void): () => void;
336
+ onToolCall(handler: (call: ToolCallPayload, envelope: CompanionEnvelope) => void): () => void;
320
337
  /** Convenience: subscribe to errors the companion surfaces — server-side agent
321
338
  * errors and stream failures (e.g. a permanent `stream_unauthorized`) both
322
339
  * arrive as `control.error`. Returns an unsubscribe fn. */
@@ -324,9 +341,14 @@ export declare class CompanionClient {
324
341
  code: string;
325
342
  message: string;
326
343
  }, envelope: CompanionEnvelope) => void): () => void;
327
- /** Recall the memory this token is authorized to see (ranked). */
344
+ /** Recall the memory this token is authorized to see. Without `query` the
345
+ * facts come back ranked by importance/recency; with `query` the server
346
+ * runs semantic search over them (embedding-based, falls back to ranked
347
+ * when embeddings are unavailable) — e.g.
348
+ * `recall({ query: 'food preferences', limit: 10 })`. */
328
349
  recall(opts?: {
329
350
  limit?: number;
351
+ query?: string;
330
352
  }): Promise<RecalledMemory[]>;
331
353
  /** Fetch this session's recent conversation turns (oldest→newest) so a
332
354
  * reconnecting embed can restore its transcript. Distinct from `recall`
package/dist/client.js CHANGED
@@ -8,8 +8,10 @@
8
8
  // await c.sendText('how am I doing?');
9
9
  //
10
10
  // Pure, dependency-free, isomorphic (browser + Node 18+ fetch). The transport is
11
- // an internal detail when the WebSocket plane lands it slots in behind this
12
- // same API. Protocol types are shared with the server via companion-protocol.
11
+ // an internal detail: the default REST/SSE plane, or an opt-in WebSocket plane
12
+ // (ws-transport.ts) that degrades back to SSE on failure both sit behind this
13
+ // same API. Protocol types are a self-contained copy of the server's contract,
14
+ // kept in lockstep by protocol.drift.test.ts.
13
15
  import { PROTOCOL_VERSION } from './protocol.js';
14
16
  import { parseSse } from './sse.js';
15
17
  import { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_VISUAL_TOOLS, AVATAR_VISUAL_TOOL_NAMES } from './call.js';
@@ -63,6 +65,45 @@ export class CompanionClient {
63
65
  get sessionId() {
64
66
  return this._session;
65
67
  }
68
+ /** Swap the bearer token used by every subsequent request and stream
69
+ * reconnect. Session tokens expire (default 1h) — call this when your
70
+ * backend re-mints one, or wire `onAuthError` to do it on demand. */
71
+ setToken(token) {
72
+ if (!token)
73
+ throw new CompanionError('setToken: token is required', 0, 'missing_option');
74
+ this.opts.token = token;
75
+ }
76
+ // In-flight onAuthError call, shared so concurrent 401s trigger ONE refresh.
77
+ refreshing = null;
78
+ /** Run opts.onAuthError (deduped), apply the fresh token, and report whether
79
+ * the caller should retry. Never throws — a failed refresh means "surface
80
+ * the original 401". */
81
+ async tryRefreshToken() {
82
+ const refresh = this.opts.onAuthError;
83
+ if (!refresh)
84
+ return false;
85
+ this.refreshing ??= Promise.resolve()
86
+ .then(refresh)
87
+ .catch(() => null)
88
+ .finally(() => {
89
+ this.refreshing = null;
90
+ });
91
+ const token = await this.refreshing;
92
+ if (!token)
93
+ return false;
94
+ this.opts.token = token;
95
+ return true;
96
+ }
97
+ /** doFetch with one transparent retry after a 401-triggered token refresh.
98
+ * Everything except the stream loop (which has its own auth handling) goes
99
+ * through here. */
100
+ async fetchAuthed(url, init) {
101
+ const res = await this.doFetch(url, init);
102
+ if (res.status !== 401 || !(await this.tryRefreshToken()))
103
+ return res;
104
+ init.headers.Authorization = `Bearer ${this.opts.token}`;
105
+ return this.doFetch(url, init);
106
+ }
66
107
  url(path) {
67
108
  return this.opts.baseUrl.replace(/\/+$/, '') + path;
68
109
  }
@@ -75,7 +116,7 @@ export class CompanionClient {
75
116
  return this._session;
76
117
  }
77
118
  async postJson(path, body) {
78
- const res = await this.doFetch(this.url(path), {
119
+ const res = await this.fetchAuthed(this.url(path), {
79
120
  method: 'POST',
80
121
  headers: this.jsonHeaders(),
81
122
  body: JSON.stringify(body)
@@ -151,7 +192,7 @@ export class CompanionClient {
151
192
  * dedups by id). Falls back to buffered semantics if the server ever
152
193
  * responds with plain JSON (e.g. an old deployment). */
153
194
  async sendTextStreaming(sessionId, body) {
154
- const res = await this.doFetch(this.url(`/api/companion/session/${sessionId}/input`), {
195
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${sessionId}/input`), {
155
196
  method: 'POST',
156
197
  headers: this.jsonHeaders(),
157
198
  body: JSON.stringify({ ...body, stream: true })
@@ -347,7 +388,7 @@ export class CompanionClient {
347
388
  .slice(-60)
348
389
  .map((t) => ({ role: t.role, text: t.text.slice(0, 500) }));
349
390
  try {
350
- const res = await this.doFetch(this.url(`/api/companion/session/${id}/end`), {
391
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${id}/end`), {
351
392
  method: 'POST',
352
393
  headers: this.jsonHeaders(),
353
394
  body: JSON.stringify({ transcript }),
@@ -481,10 +522,20 @@ export class CompanionClient {
481
522
  }, env);
482
523
  });
483
524
  }
484
- /** Recall the memory this token is authorized to see (ranked). */
525
+ /** Recall the memory this token is authorized to see. Without `query` the
526
+ * facts come back ranked by importance/recency; with `query` the server
527
+ * runs semantic search over them (embedding-based, falls back to ranked
528
+ * when embeddings are unavailable) — e.g.
529
+ * `recall({ query: 'food preferences', limit: 10 })`. */
485
530
  async recall(opts) {
486
- const qs = opts?.limit ? `?limit=${encodeURIComponent(opts.limit)}` : '';
487
- const res = await this.doFetch(this.url(`/api/companion/memory${qs}`), {
531
+ const params = new URLSearchParams();
532
+ if (opts?.limit)
533
+ params.set('limit', String(opts.limit));
534
+ if (opts?.query?.trim())
535
+ params.set('q', opts.query.trim());
536
+ const query = params.toString();
537
+ const qs = query ? `?${query}` : '';
538
+ const res = await this.fetchAuthed(this.url(`/api/companion/memory${qs}`), {
488
539
  headers: { Authorization: `Bearer ${this.opts.token}` }
489
540
  });
490
541
  const json = (await res.json().catch(() => null));
@@ -501,7 +552,7 @@ export class CompanionClient {
501
552
  async history(opts) {
502
553
  const id = this.requireSession();
503
554
  const qs = opts?.limit ? `?limit=${encodeURIComponent(opts.limit)}` : '';
504
- const res = await this.doFetch(this.url(`/api/companion/session/${encodeURIComponent(id)}/history${qs}`), { headers: { Authorization: `Bearer ${this.opts.token}` } });
555
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/history${qs}`), { headers: { Authorization: `Bearer ${this.opts.token}` } });
505
556
  const json = (await res.json().catch(() => null));
506
557
  if (!res.ok || !json || json.ok === false) {
507
558
  throw new CompanionError(json?.error || `history failed (${res.status})`, res.status);
@@ -514,7 +565,7 @@ export class CompanionClient {
514
565
  * returned. Requires a live session. */
515
566
  async setModalities(modalities) {
516
567
  const id = this.requireSession();
517
- const res = await this.doFetch(this.url(`/api/companion/session/${encodeURIComponent(id)}/modalities`), {
568
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/modalities`), {
518
569
  method: 'POST',
519
570
  headers: {
520
571
  Authorization: `Bearer ${this.opts.token}`,
@@ -533,7 +584,7 @@ export class CompanionClient {
533
584
  * it). Cheap; call it on a timer for a background tab. Requires a live session. */
534
585
  async ping() {
535
586
  const id = this.requireSession();
536
- const res = await this.doFetch(this.url(`/api/companion/session/${encodeURIComponent(id)}/ping`), { method: 'POST', headers: { Authorization: `Bearer ${this.opts.token}` } });
587
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/ping`), { method: 'POST', headers: { Authorization: `Bearer ${this.opts.token}` } });
537
588
  if (!res.ok) {
538
589
  const json = (await res.json().catch(() => null));
539
590
  throw new CompanionError(json?.error || `ping failed (${res.status})`, res.status);
@@ -578,7 +629,7 @@ export class CompanionClient {
578
629
  * and the /models asset is CORS-enabled, so a cross-origin renderer can fetch
579
630
  * the VRM directly. Reflects the user's live choice (built-in or custom). */
580
631
  async getAvatar() {
581
- const res = await this.doFetch(this.url('/api/companion/avatar'), {
632
+ const res = await this.fetchAuthed(this.url('/api/companion/avatar'), {
582
633
  headers: { Authorization: `Bearer ${this.opts.token}` }
583
634
  });
584
635
  const json = (await res.json().catch(() => null));
@@ -599,7 +650,7 @@ export class CompanionClient {
599
650
  * which subsumes it). Same data the companion gives when asked "what's my
600
651
  * balance?". */
601
652
  async getWallet() {
602
- const res = await this.doFetch(this.url('/api/companion/wallet'), {
653
+ const res = await this.fetchAuthed(this.url('/api/companion/wallet'), {
603
654
  headers: { Authorization: `Bearer ${this.opts.token}` }
604
655
  });
605
656
  const json = (await res.json().catch(() => null));
@@ -715,7 +766,7 @@ export class CompanionClient {
715
766
  * session tokens only, like `confirmAction`. */
716
767
  async pendingConfirms() {
717
768
  const sessionId = this.requireSession();
718
- const res = await this.doFetch(this.url(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`), { headers: { Authorization: `Bearer ${this.opts.token}` } });
769
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`), { headers: { Authorization: `Bearer ${this.opts.token}` } });
719
770
  const json = (await res.json().catch(() => null));
720
771
  if (!res.ok || !json || json.ok === false) {
721
772
  throw new CompanionError(json?.error || `pending confirms failed (${res.status})`, res.status);
@@ -924,11 +975,21 @@ export class CompanionClient {
924
975
  async loop() {
925
976
  const id = this.requireSession();
926
977
  let backoff = 500;
978
+ let authRefreshes = 0; // consecutive 401→refresh cycles without a healthy connect
927
979
  while (this.streaming) {
928
980
  this.abort = new AbortController();
929
981
  try {
930
982
  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 });
931
983
  if (res.status === 401 || res.status === 403) {
984
+ // 401 = the token itself is bad — usually an EXPIRED session token
985
+ // (they live ~1h). With an onAuthError hook this is recoverable:
986
+ // refresh and reconnect instead of going permanently silent. The
987
+ // counter stops a hot loop when the hook keeps returning a token
988
+ // the server keeps rejecting.
989
+ if (res.status === 401 && authRefreshes < 3 && (await this.tryRefreshToken())) {
990
+ authRefreshes += 1;
991
+ continue;
992
+ }
932
993
  // Permanent: the token can't subscribe (most often it lacks the
933
994
  // `events.subscribe` scope; also revoked / wrong user). Retrying is
934
995
  // futile and would silently swallow every reply — surface it as a
@@ -955,6 +1016,7 @@ export class CompanionClient {
955
1016
  continue;
956
1017
  }
957
1018
  backoff = 500; // healthy connection resets backoff
1019
+ authRefreshes = 0;
958
1020
  await this.consume(res.body);
959
1021
  }
960
1022
  catch {
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_
5
5
  export type { CompanionCall, CompanionCallOptions, VoiceToolBridge } from './call.js';
6
6
  export { COMPANION_SCOPES, COMPANION_MODALITIES, SENSITIVE_SCOPES, REPRESENT_SCOPES, DEFAULT_SCOPES, DEFAULT_MODALITIES, isSensitiveScope, isRepresentScope, hasScope } from './scopes.js';
7
7
  export type { CompanionScope, CompanionModality } from './scopes.js';
8
- export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, AudioClipPayload, ExpressionPayload, TypingPayload, VoiceInjectPayload, UsagePayload } from './protocol.js';
8
+ export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, ToolCallPayload, AudioClipPayload, ExpressionPayload, TypingPayload, VoiceInjectPayload, UsagePayload } from './protocol.js';
9
+ export { PROTOCOL_VERSION, INBOUND_TYPES, OUTBOUND_TYPES } from './protocol.js';
9
10
  /** Create a companion client. */
10
11
  export declare function createCompanion(opts: CompanionClientOptions): CompanionClient;
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Companion SDK — public entry. Embed the Pouchy companion in a third-party
2
- // surface (web / app / game / hardware) over the REST/SSE plane.
2
+ // surface (web / app / game / hardware) over the REST/SSE plane (with an
3
+ // optional WebSocket transport that degrades to SSE).
3
4
  //
4
5
  // import { createCompanion } from '$lib/companion-sdk';
5
6
  // const c = createCompanion({ baseUrl: 'https://pouchy.ai', token: PAT });
@@ -12,6 +13,9 @@ import { CompanionClient } from './client.js';
12
13
  export { CompanionClient, CompanionError, pouchyBrandIconUrl } from './client.js';
13
14
  export { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_VISUAL_TOOLS, AVATAR_VISUAL_TOOL_NAMES } from './call.js';
14
15
  export { COMPANION_SCOPES, COMPANION_MODALITIES, SENSITIVE_SCOPES, REPRESENT_SCOPES, DEFAULT_SCOPES, DEFAULT_MODALITIES, isSensitiveScope, isRepresentScope, hasScope } from './scopes.js';
16
+ // Runtime protocol constants — so a host can assert PROTOCOL_VERSION or
17
+ // enumerate/validate the event vocabulary without hard-coding the strings.
18
+ export { PROTOCOL_VERSION, INBOUND_TYPES, OUTBOUND_TYPES } from './protocol.js';
15
19
  /** Create a companion client. */
16
20
  export function createCompanion(opts) {
17
21
  return new CompanionClient(opts);
@@ -76,6 +76,18 @@ export interface PendingConfirm {
76
76
  /** Epoch ms when the companion asked. */
77
77
  createdAt: number;
78
78
  }
79
+ /** Payload of a `companion.tool_call` event — the companion is asking the host
80
+ * to run one of the app-declared tools it was given at `hello`. `args` is the
81
+ * raw JSON string the model produced (parse it yourself); reply by posting the
82
+ * result back with `client.sendToolResult(id, …)`. */
83
+ export interface ToolCallPayload {
84
+ /** Correlation id — echo it back on the tool result. */
85
+ id: string;
86
+ /** The declared tool name the model chose. */
87
+ name: string;
88
+ /** The model's arguments as a raw JSON string. */
89
+ args: string;
90
+ }
79
91
  /** Payload of a `companion.ui_update` event — a live change to an
80
92
  * already-rendered Instant UI panel: write each `{ key, value }` into the
81
93
  * panel's state bag (re-evaluating templates / progress / `when`), without
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/companion-sdk",
3
- "version": "0.20.0",
3
+ "version": "0.22.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",