@pouchy_ai/companion-sdk 0.24.0 → 0.25.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,37 @@ a protocol bump is always called out explicitly here.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.25.0] - 2026-07-11
16
+
17
+ ### Added
18
+
19
+ - **`sendText(text, { awaitReply: true })` — await the reply as the return
20
+ value.** Resolves with `{ seq, text, envelope }` (a new exported
21
+ `SendTextReply` type) once the turn completes: `text` is the authoritative
22
+ final assistant text (the same value `onMessage` receives), `envelope` the
23
+ full `companion.message` envelope. Request/response hosts — a CLI REPL, an
24
+ HTTP handler, a test — no longer hand-roll the resolve-on-message +
25
+ timeout gate every integration was writing. It rides the same streaming
26
+ request (the terminal `done` frame carries the envelope), so it works
27
+ without `start()`; `onMessage`/`onDelta` subscribers still fire as usual.
28
+ With `stream: false` (or an old server that answers plain JSON) it falls
29
+ back to the event stream — which then does need `start()` — resolving with
30
+ the next `companion.message` or rejecting with a client-side
31
+ `CompanionError` `code: 'reply_timeout'` after `replyTimeoutMs`
32
+ (default 60 000 ms).
33
+
34
+ ### Docs
35
+
36
+ - OpenAPI spec corrections (served at `/api/companion/openapi.json`): the
37
+ memory-write body field is `content` (was wrongly documented as `text`),
38
+ knowledge ingest takes `name`/`kind`/`locale` (was `title`), the
39
+ modalities enum is `text|voice|call|files` (`avatar` never existed),
40
+ `GET …/confirm` (pending confirmations) is now documented, the session
41
+ handshake body documents `handles`/`contextKinds`/`visitor`, and memory
42
+ scopes use the real namespaced strings (`memory.read:app` …).
43
+ - The `/sdk` reference described the call handle as having `.end()` — the
44
+ method is `close()`.
45
+
15
46
  ## [0.24.0] - 2026-07-11
16
47
 
17
48
  ### Added
package/README.md CHANGED
@@ -96,6 +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); only the `stream: false` / old-server fallback needs the event stream, and it rejects with `code: 'reply_timeout'` after `replyTimeoutMs` (default 60s). |
99
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. |
100
101
  | `ingestKnowledge({ text, name, … })` / `ingestFile(file)` | Push documents into the user's knowledge base ("My materials"); needs `memory.write:core`. |
101
102
  | `history({ limit? })` | Fetch the session transcript — restore chat on reload. |
package/dist/client.d.ts CHANGED
@@ -181,6 +181,15 @@ export interface EndSessionResult {
181
181
  facts?: number;
182
182
  skipped?: string;
183
183
  }
184
+ /** What `sendText(text, { awaitReply: true })` resolves with: the completed
185
+ * turn's reply. `text` is the authoritative final assistant text (same value
186
+ * onMessage receives); `envelope` is the full `companion.message` envelope
187
+ * for hosts that want the id/ts. */
188
+ export interface SendTextReply {
189
+ seq: number | null;
190
+ text: string;
191
+ envelope: CompanionEnvelope;
192
+ }
184
193
  type Handler = (envelope: CompanionEnvelope) => void;
185
194
  /** Token-streaming subscriber (P1-2). `chunk` is a raw text delta of the reply
186
195
  * being generated; `meta.reset` (with an empty chunk) tells the renderer to
@@ -244,13 +253,38 @@ export declare class CompanionClient {
244
253
  * request — delta subscribers fire as chunks arrive, and onMessage still
245
254
  * fires exactly ONCE with the authoritative final text (the streamed copy
246
255
  * is emitted locally; the event-stream replay of the same envelope is
247
- * deduplicated by id). Pass `stream: false` to force the buffered path. */
256
+ * deduplicated by id). Pass `stream: false` to force the buffered path.
257
+ *
258
+ * AWAIT THE REPLY (request/response hosts — a REPL, an HTTP handler, a
259
+ * test): pass `awaitReply: true` and the promise resolves with the final
260
+ * reply `{ seq, text, envelope }` — no onMessage wiring, no hand-rolled
261
+ * turn gate. It rides the same streaming request (the terminal `done`
262
+ * frame carries the authoritative envelope), so it works without start();
263
+ * onMessage/onDelta subscribers still fire as usual. Only when the reply
264
+ * can't ride the request (`stream: false`, or an old server) does it fall
265
+ * back to the event stream — which then DOES need start() — resolving with
266
+ * the next `companion.message`, or rejecting with code `reply_timeout`
267
+ * after `replyTimeoutMs` (default 60s). */
268
+ sendText(text: string, opts: {
269
+ awaitReply: true;
270
+ images?: string[];
271
+ stream?: boolean;
272
+ replyTimeoutMs?: number;
273
+ }): Promise<SendTextReply>;
248
274
  sendText(text: string, opts?: {
249
275
  images?: string[];
250
276
  stream?: boolean;
277
+ awaitReply?: boolean;
278
+ replyTimeoutMs?: number;
251
279
  }): Promise<{
252
280
  seq: number | null;
253
281
  }>;
282
+ /** The awaitReply leg of sendText. Prefers the in-request answer (the
283
+ * streaming `done` frame's envelope); falls back to the next
284
+ * `companion.message` off the event stream, bounded by a timeout. The
285
+ * fallback subscription opens BEFORE the POST so a fast event-stream reply
286
+ * can't slip through the gap. */
287
+ private sendTextAwaitingReply;
254
288
  /** The streaming leg of sendText: parse the POST response's SSE frames —
255
289
  * `delta` / `reset` → delta subscribers, `done` → resolve (emitting the
256
290
  * final envelope locally so onMessage fires immediately and the log replay
package/dist/client.js CHANGED
@@ -177,26 +177,58 @@ export class CompanionClient {
177
177
  });
178
178
  return { pairId: json.pairId ?? null };
179
179
  }
180
- /** Send a user text turn, optionally with images (data URLs) for a multimodal
181
- * turn — images need the `files` scope. The reply arrives on the event stream
182
- * as a `companion.message` — subscribe via onMessage()/start() to receive it.
183
- *
184
- * TOKEN STREAMING (P1-2): when any onDelta() subscriber is registered (or
185
- * `opts.stream` is true), the reply streams token-by-token on this very
186
- * request — delta subscribers fire as chunks arrive, and onMessage still
187
- * fires exactly ONCE with the authoritative final text (the streamed copy
188
- * is emitted locally; the event-stream replay of the same envelope is
189
- * deduplicated by id). Pass `stream: false` to force the buffered path. */
190
180
  async sendText(text, opts) {
191
181
  const id = this.requireSession();
192
- const wantStream = opts?.stream ?? this.deltaHandlers.size > 0;
193
182
  const body = { text, ...(opts?.images?.length ? { images: opts.images } : {}) };
183
+ if (opts?.awaitReply)
184
+ return this.sendTextAwaitingReply(id, body, opts);
185
+ const wantStream = opts?.stream ?? this.deltaHandlers.size > 0;
194
186
  if (!wantStream) {
195
187
  const json = await this.postJson(`/api/companion/session/${id}/input`, body);
196
188
  return { seq: json.seq ?? null };
197
189
  }
198
190
  return this.sendTextStreaming(id, body);
199
191
  }
192
+ /** The awaitReply leg of sendText. Prefers the in-request answer (the
193
+ * streaming `done` frame's envelope); falls back to the next
194
+ * `companion.message` off the event stream, bounded by a timeout. The
195
+ * fallback subscription opens BEFORE the POST so a fast event-stream reply
196
+ * can't slip through the gap. */
197
+ async sendTextAwaitingReply(sessionId, body, opts) {
198
+ const timeoutMs = opts?.replyTimeoutMs ?? 60_000;
199
+ let resolveFallback;
200
+ const fallback = new Promise((res) => (resolveFallback = res));
201
+ const off = this.on('companion.message', (env) => resolveFallback(env));
202
+ let timer;
203
+ try {
204
+ let seq = null;
205
+ let envelope;
206
+ if (opts?.stream === false) {
207
+ const json = await this.postJson(`/api/companion/session/${sessionId}/input`, body);
208
+ seq = json.seq ?? null;
209
+ }
210
+ else {
211
+ const r = await this.sendTextStreaming(sessionId, body);
212
+ seq = r.seq;
213
+ envelope = r.envelope;
214
+ }
215
+ if (!envelope) {
216
+ envelope = await Promise.race([
217
+ fallback,
218
+ new Promise((_, reject) => {
219
+ timer = setTimeout(() => reject(new CompanionError(`timed out after ${timeoutMs}ms waiting for the reply — buffered mode needs start() polling the event stream`, 0, 'reply_timeout')), timeoutMs);
220
+ })
221
+ ]);
222
+ }
223
+ const replyText = envelope.payload?.text;
224
+ return { seq, text: typeof replyText === 'string' ? replyText : '', envelope };
225
+ }
226
+ finally {
227
+ off();
228
+ if (timer !== undefined)
229
+ clearTimeout(timer);
230
+ }
231
+ }
200
232
  /** The streaming leg of sendText: parse the POST response's SSE frames —
201
233
  * `delta` / `reset` → delta subscribers, `done` → resolve (emitting the
202
234
  * final envelope locally so onMessage fires immediately and the log replay
@@ -256,7 +288,7 @@ export class CompanionClient {
256
288
  // latency; the event-stream replay of the same id is deduped.
257
289
  if (d.envelope && typeof d.envelope === 'object')
258
290
  this.emit(d.envelope);
259
- return { seq: d.seq ?? null };
291
+ return { seq: d.seq ?? null, envelope: d.envelope ?? undefined };
260
292
  }
261
293
  }
262
294
  if (done)
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, ToolCallEvent, WorldStateInput, CompanionAvatar, CompanionWallet, WalletBalance, BrandIconSize, EndSessionResult } from './client.js';
3
+ export type { CompanionClientOptions, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, ToolCallEvent, WorldStateInput, CompanionAvatar, CompanionWallet, WalletBalance, BrandIconSize, EndSessionResult, SendTextReply } 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 { COMPANION_SCOPES, COMPANION_MODALITIES, SENSITIVE_SCOPES, REPRESENT_SCOPES, DEFAULT_SCOPES, DEFAULT_MODALITIES, isSensitiveScope, isRepresentScope, hasScope } from './scopes.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pouchy_ai/companion-sdk",
3
- "version": "0.24.0",
4
- "description": "Embed the Pouchy companion \u2014 chat, voice, tools, memory, live world-state, instant UI, and agent-to-agent messaging \u2014 in any app, game, or site.",
3
+ "version": "0.25.0",
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",
7
7
  "homepage": "https://pouchy.ai",